summaryrefslogtreecommitdiffstats
path: root/src/com/android/wallpaper/util/WallpaperCropUtils.java
blob: eaf0e4a8dbc7b632f441f6a2c179a5fa6071e59c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.wallpaper.util;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.Display;
import android.view.View;

import com.android.systemui.shared.system.WallpaperManagerCompat;

/**
 * Static utility methods for wallpaper cropping operations.
 */
public final class WallpaperCropUtils {

    private static final float WALLPAPER_SCREENS_SPAN = 2f;
    private static final float ASPECT_RATIO_LANDSCAPE = 16 / 10f;
    private static final float ASPECT_RATIO_PORTRAIT = 10 / 16f;
    private static final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.2f;
    private static final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.5f;

    // Suppress default constructor for noninstantiability.
    private WallpaperCropUtils() {
        throw new AssertionError();
    }

    /**
     * Calculates parallax travel (i.e., extra width) for a screen with the given resolution.
     */
    public static float wallpaperTravelToScreenWidthRatio(int width, int height) {
        float aspectRatio = width / (float) height;

        // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.2 * screen width
        // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.5 * screen width
        // We will use these two data points to extrapolate how much the wallpaper parallax effect
        // to span (i.e., travel) at any aspect ratio. We use the following two linear formulas,
        // where
        // the coefficient on x is the aspect ratio (width/height):
        //   (16/10)x + y = 1.2
        //   (10/16)x + y = 1.5
        // We solve for x and y and end up with a final formula:
        float x = (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE
                - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT)
                / (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
        float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
        return x * aspectRatio + y;
    }

    /**
     * Calculates ideal crop surface size for a device such that there is room for parallax in both
     * landscape and portrait screen orientations.
     */
    public static Point getDefaultCropSurfaceSize(Resources resources, Display display) {
        Point minDims = new Point();
        Point maxDims = new Point();
        display.getCurrentSizeRange(minDims, maxDims);

        int maxDim = Math.max(maxDims.x, maxDims.y);
        int minDim = Math.max(minDims.x, minDims.y);

        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
            Point realSize = new Point();
            display.getRealSize(realSize);
            maxDim = Math.max(realSize.x, realSize.y);
            minDim = Math.min(realSize.x, realSize.y);
        }

        return calculateCropSurfaceSize(resources, maxDim, minDim);
    }

    /**
     * Calculates ideal crop surface size for a surface of dimensions maxDim x minDim such that
     * there is room for parallax in both* landscape and portrait screen orientations.
     */
    public static Point calculateCropSurfaceSize(Resources resources, int maxDim, int minDim) {
        final int defaultWidth, defaultHeight;
        if (resources.getConfiguration().smallestScreenWidthDp >= 720) {
            defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
            defaultHeight = maxDim;
        } else {
            defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
            defaultHeight = maxDim;
        }

        return new Point(defaultWidth, defaultHeight);
    }

    /**
     * Calculates the relative position between an outer and inner rectangle when the outer one is
     * center-cropped to the inner one.
     *
     * @param outer      Size of outer rectangle as a Point (x,y).
     * @param inner      Size of inner rectangle as a Point (x,y).
     * @param alignStart Whether the inner rectangle should be aligned to the start of the layout
     *                   with the outer one and ignore horizontal centering.
     * @param isRtl      Whether the layout direction is RTL (or false for LTR).
     * @return Position relative to the top left corner of the outer rectangle, where the size of
     * each rectangle is represented by Points, in coordinates (x,y) relative to the outer rectangle
     * where the top left corner is (0,0)
     * @throws IllegalArgumentException if inner rectangle is not contained within outer rectangle
     *                                  which would return a position with at least one negative
     *                                  coordinate.
     */
    public static Point calculateCenterPosition(Point outer, Point inner, boolean alignStart,
            boolean isRtl) {
        if (inner.x > outer.x || inner.y > outer.y) {
            throw new IllegalArgumentException("Inner rectangle " + inner + " should be contained"
                    + " completely within the outer rectangle " + outer + ".");
        }

        Point relativePosition = new Point();

        if (alignStart) {
            relativePosition.x = isRtl ? outer.x - inner.x : 0;
        } else {
            relativePosition.x = Math.round((outer.x - inner.x) / 2f);
        }
        relativePosition.y = Math.round((outer.y - inner.y) / 2f);

        return relativePosition;
    }

    /**
     * Calculates the minimum zoom such that the maximum surface area of the outer rectangle is
     * visible within the inner rectangle.
     *
     * @param outer Size of outer rectangle as a Point (x,y).
     * @param inner Size of inner rectangle as a Point (x,y).
     */
    public static float calculateMinZoom(Point outer, Point inner) {
        float minZoom;
        if (inner.x / (float) inner.y > outer.x / (float) outer.y) {
            minZoom = inner.x / (float) outer.x;
        } else {
            minZoom = inner.y / (float) outer.y;
        }
        return minZoom;
    }


    /**
     * Calculates the center position of a wallpaper of the given size, based on a "crop surface"
     * (with extra width to account for parallax) superimposed on the screen. Trying to show as
     * much of the wallpaper as possible on the crop surface and align screen to crop surface such
     * that the centered wallpaper matches what would be seen by the user in the left-most home
     * screen.
     *
     * @param wallpaperSize full size of the wallpaper
     * @param visibleWallpaperRect visible area available for the wallpaper
     * @return a point corresponding to the position of wallpaper that should be in the center
     *      of the screen.
     */
    public static PointF calculateDefaultCenter(Context context, Point wallpaperSize,
            Rect visibleWallpaperRect) {

        WallpaperCropUtils.adjustCurrentWallpaperCropRect(context, wallpaperSize,
                visibleWallpaperRect);

        return new PointF(visibleWallpaperRect.centerX(),
                visibleWallpaperRect.centerY());
    }

    /**
     * Calculates the rectangle to crop a wallpaper of the given size, and considering the given
     * scrollX and scrollY offsets
     * @param wallpaperZoom zoom applied to the raw wallpaper image
     * @param wallpaperSize full ("raw") wallpaper size
     * @param defaultCropSurfaceSize @see #getDefaultCropSurfaceSize(Resources, Display)
     * @param targetHostSize target size where the wallpaper will be rendered (eg, the display size)
     * @param scrollX x-axis offset the cropping area from the wallpaper's 0,0 position
     * @param scrollY y-axis offset the cropping area from the wallpaper's 0,0 position
     * @return a Rect representing the area of the wallpaper to crop.
     */
    public static Rect calculateCropRect(Context context, float wallpaperZoom, Point wallpaperSize,
            Point defaultCropSurfaceSize, Point targetHostSize, int scrollX, int scrollY) {
        // Calculate Rect of wallpaper in physical pixel terms (i.e., scaled to current zoom).
        int scaledWallpaperWidth = Math.round(wallpaperSize.x * wallpaperZoom);
        int scaledWallpaperHeight = Math.round(wallpaperSize.y * wallpaperZoom);
        Rect rect = new Rect();
        rect.set(0, 0, scaledWallpaperWidth, scaledWallpaperHeight);

        // Crop rect should start off as the visible screen and then include extra width and height
        // if available within wallpaper at the current zoom.
        Rect cropRect = new Rect(scrollX, scrollY, scrollX + targetHostSize.x,
                scrollY + targetHostSize.y);

        int extraWidth = defaultCropSurfaceSize.x - targetHostSize.x;
        int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - targetHostSize.y) / 2f);

        // Try to increase size of screenRect to include extra width depending on the layout
        // direction.
        if (isRtl(context)) {
            cropRect.left = Math.max(cropRect.left - extraWidth, rect.left);
        } else {
            cropRect.right = Math.min(cropRect.right + extraWidth, rect.right);
        }

        // Try to increase the size of the cropRect to to include extra height.
        int availableExtraHeightTop = cropRect.top - Math.max(
                rect.top,
                cropRect.top - extraHeightTopAndBottom);
        int availableExtraHeightBottom = Math.min(
                rect.bottom,
                cropRect.bottom + extraHeightTopAndBottom) - cropRect.bottom;

        int availableExtraHeightTopAndBottom =
                Math.min(availableExtraHeightTop, availableExtraHeightBottom);
        cropRect.top -= availableExtraHeightTopAndBottom;
        cropRect.bottom += availableExtraHeightTopAndBottom;

        return cropRect;
    }

    /**
     * Calculates the center area of the outer rectangle which is visible in the inner rectangle
     * after applying the minimum zoom.
     *
     * @param outer the size of outer rectangle as a Point (x,y).
     * @param inner the size of inner rectangle as a Point (x,y).
     */
    public static Rect calculateVisibleRect(Point outer, Point inner) {
        PointF visibleRectCenter = new PointF(outer.x / 2f, outer.y / 2f);
        if (inner.x / (float) inner.y > outer.x / (float) outer.y) {
            float minZoom = inner.x / (float) outer.x;
            float visibleRectHeight = inner.y / minZoom;
            return new Rect(0, (int) (visibleRectCenter.y - visibleRectHeight / 2),
                    outer.x, (int) (visibleRectCenter.y + visibleRectHeight / 2));
        } else {
            float minZoom = inner.y / (float) outer.y;
            float visibleRectWidth = inner.x / minZoom;
            return new Rect((int) (visibleRectCenter.x - visibleRectWidth / 2),
                    0, (int) (visibleRectCenter.x + visibleRectWidth / 2), outer.y);
        }
    }

    public static void adjustCurrentWallpaperCropRect(Context context, Point assetDimensions,
            Rect cropRect) {
        adjustCropRect(context, cropRect, true /* zoomIn */);
    }

    /** Adjust the crop rect by zooming in with systemWallpaperMaxScale. */
    public static void adjustCropRect(Context context, Rect cropRect, boolean zoomIn) {
        float centerX = cropRect.centerX();
        float centerY = cropRect.centerY();
        float width = cropRect.width();
        float height = cropRect.height();
        float systemWallpaperMaxScale = getSystemWallpaperMaximumScale(context);
        float scale = zoomIn ? systemWallpaperMaxScale : 1.0f / systemWallpaperMaxScale;

        // Adjust the rect according to the system wallpaper's maximum scale.
        int left = (int) (centerX - (width / 2) / scale);
        int top = (int) (centerY - (height / 2) / scale);
        int right = (int) (centerX + (width / 2) / scale);
        int bottom = (int) (centerY + (height / 2) / scale);
        cropRect.set(left, top, right, bottom);
    }

    /** Adjust the given Point, representing a size by  systemWallpaperMaxScale. */
    public static void scaleSize(Context context, Point size) {
        float systemWallpaperMaxScale = getSystemWallpaperMaximumScale(context);
        size.set((int) (size.x * systemWallpaperMaxScale),
                (int) (size.y * systemWallpaperMaxScale));
    }
    /**
     * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms
     * (i.e., scaled to current zoom) when the wallpaper is laid on a fullscreen view.
     */
    public static Rect calculateCropRect(Context context, Display display, Point rawWallpaperSize,
            Rect visibleRawWallpaperRect, float wallpaperZoom) {
        Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(display);
        Point defaultCropSize = WallpaperCropUtils.getDefaultCropSurfaceSize(
                context.getResources(), display);
        return calculateCropRect(context, screenSize, defaultCropSize, rawWallpaperSize,
                visibleRawWallpaperRect, wallpaperZoom);
    }

    /**
     * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms
     * (i.e., scaled to current zoom).
     *
     * @param hostViewSize            the size of the view hosting the wallpaper as a Point (x,y).
     * @param cropSize                the default size of the crop as a Point (x,y).
     * @param rawWallpaperSize        the size of the raw wallpaper as a Point (x,y).
     * @param visibleRawWallpaperRect the area of the raw wallpaper which is expected to see.
     * @param wallpaperZoom           the factor which is used to scale the raw wallpaper.
     */
    public static Rect calculateCropRect(Context context, Point hostViewSize, Point cropSize,
            Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom) {
        int scrollX = (int) (visibleRawWallpaperRect.left * wallpaperZoom);
        int scrollY = (int) (visibleRawWallpaperRect.top * wallpaperZoom);

        return calculateCropRect(context, wallpaperZoom, rawWallpaperSize, cropSize, hostViewSize,
                scrollX, scrollY);
    }

    /**
     * Resize the wallpaper size so it's new size fits in a outWidth by outHeight rectangle.
     *
     * @param wallpaperSize Rectangle with the current wallpaper size. It will be resized.
     * @param outWidth      the width of the rectangle in which the wallpaperSize needs to fit.
     * @param outHeight     the height of the rectangle in which the wallpaperSize needs to fit.
     */
    public static void fitToSize(Rect wallpaperSize, int outWidth, int outHeight) {
        if (wallpaperSize.isEmpty()) {
            return;
        }
        float maxSizeOut = Math.max(outWidth, outHeight);
        float maxSizeIn = Math.max(wallpaperSize.width(), wallpaperSize.height());
        float scale = maxSizeOut / maxSizeIn;

        // Scale the wallpaper size
        if (scale != 1.0f) {
            wallpaperSize.left = (int) (wallpaperSize.left * scale + 0.5f);
            wallpaperSize.top = (int) (wallpaperSize.top * scale + 0.5f);
            wallpaperSize.right = (int) (wallpaperSize.right * scale + 0.5f);
            wallpaperSize.bottom = (int) (wallpaperSize.bottom * scale + 0.5f);
        }
    }

    /**
     * Get the system wallpaper's maximum scale value.
     */
    public static float getSystemWallpaperMaximumScale(Context context) {
        return WallpaperManagerCompat.getWallpaperZoomOutMaxScale(context);
    }

    /**
     * Returns whether layout direction is RTL (or false for LTR). Since native RTL layout support
     * was added in API 17, returns false for versions lower than 17.
     */
    public static boolean isRtl(Context context) {
        return context.getResources().getConfiguration().getLayoutDirection()
                == View.LAYOUT_DIRECTION_RTL;
    }
}