1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.wallpaper.util;
17 
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.graphics.Point;
21 import android.graphics.PointF;
22 import android.graphics.Rect;
23 import android.os.Build.VERSION;
24 import android.os.Build.VERSION_CODES;
25 import android.view.Display;
26 
27 import com.android.wallpaper.config.BaseFlags;
28 import com.android.wallpaper.module.InjectorProvider;
29 
30 /**
31  * Static utility methods for wallpaper cropping operations.
32  */
33 public final class WallpaperCropUtils {
34 
35     private static final float WALLPAPER_SCREENS_SPAN = 2f;
36     private static final float ASPECT_RATIO_LANDSCAPE = 16 / 10f;
37     private static final float ASPECT_RATIO_PORTRAIT = 10 / 16f;
38     private static final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.2f;
39     private static final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.5f;
40 
41     // Suppress default constructor for noninstantiability.
WallpaperCropUtils()42     private WallpaperCropUtils() {
43         throw new AssertionError();
44     }
45 
46     /**
47      * Calculates parallax travel (i.e., extra width) for a screen with the given resolution.
48      */
wallpaperTravelToScreenWidthRatio(int width, int height)49     public static float wallpaperTravelToScreenWidthRatio(int width, int height) {
50         float aspectRatio = width / (float) height;
51 
52         // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.2 * screen width
53         // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.5 * screen width
54         // We will use these two data points to extrapolate how much the wallpaper parallax effect
55         // to span (i.e., travel) at any aspect ratio. We use the following two linear formulas,
56         // where
57         // the coefficient on x is the aspect ratio (width/height):
58         //   (16/10)x + y = 1.2
59         //   (10/16)x + y = 1.5
60         // We solve for x and y and end up with a final formula:
61         float x = (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE
62                 - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT)
63                 / (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
64         float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
65         return x * aspectRatio + y;
66     }
67 
68     /**
69      * Calculates ideal crop surface size for a device such that there is room for parallax in both
70      * landscape and portrait screen orientations.
71      */
getDefaultCropSurfaceSize(Resources resources, Display display)72     public static Point getDefaultCropSurfaceSize(Resources resources, Display display) {
73         Point minDims = new Point();
74         Point maxDims = new Point();
75         display.getCurrentSizeRange(minDims, maxDims);
76 
77         int maxDim = Math.max(maxDims.x, maxDims.y);
78         int minDim = Math.max(minDims.x, minDims.y);
79 
80         if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
81             Point realSize = new Point();
82             display.getRealSize(realSize);
83             maxDim = Math.max(realSize.x, realSize.y);
84             minDim = Math.min(realSize.x, realSize.y);
85         }
86 
87         return calculateCropSurfaceSize(resources, maxDim, minDim, display.getWidth(),
88                 display.getHeight());
89     }
90 
91     /**
92      * Calculates ideal crop surface size for a surface of dimensions maxDim x minDim such that
93      * there is room for parallax in both* landscape and portrait screen orientations.
94      */
calculateCropSurfaceSize(Resources resources, int maxDim, int minDim, int width, int height)95     public static Point calculateCropSurfaceSize(Resources resources, int maxDim, int minDim,
96             int width, int height) {
97         final int defaultWidth, defaultHeight;
98         if (resources.getConfiguration().smallestScreenWidthDp >= 720) {
99             defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
100         } else {
101             defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
102         }
103         defaultHeight = width < height ? maxDim : minDim;
104 
105         return new Point(defaultWidth, defaultHeight);
106     }
107 
108     /**
109      * Calculates the relative position between an outer and inner rectangle when the outer one is
110      * center-cropped to the inner one.
111      *
112      * @param outer      Size of outer rectangle as a Point (x,y).
113      * @param inner      Size of inner rectangle as a Point (x,y).
114      * @param alignStart Whether the inner rectangle should be aligned to the start of the layout
115      *                   with the outer one and ignore horizontal centering.
116      * @param isRtl      Whether the layout direction is RTL (or false for LTR).
117      * @return Position relative to the top left corner of the outer rectangle, where the size of
118      * each rectangle is represented by Points, in coordinates (x,y) relative to the outer rectangle
119      * where the top left corner is (0,0)
120      * @throws IllegalArgumentException if inner rectangle is not contained within outer rectangle
121      *                                  which would return a position with at least one negative
122      *                                  coordinate.
123      */
124     public static Point calculateCenterPosition(Point outer, Point inner, boolean alignStart,
125             boolean isRtl) {
126         if (inner.x > outer.x || inner.y > outer.y) {
127             throw new IllegalArgumentException("Inner rectangle " + inner + " should be contained"
128                     + " completely within the outer rectangle " + outer + ".");
129         }
130 
131         Point relativePosition = new Point();
132 
133         if (alignStart) {
134             relativePosition.x = isRtl ? outer.x - inner.x : 0;
135         } else {
136             relativePosition.x = Math.round((outer.x - inner.x) / 2f);
137         }
138         relativePosition.y = Math.round((outer.y - inner.y) / 2f);
139 
140         return relativePosition;
141     }
142 
143     /**
144      * Calculates the minimum zoom such that the maximum surface area of the outer rectangle is
145      * visible within the inner rectangle.
146      *
147      * @param outer Size of outer rectangle as a Point (x,y).
148      * @param inner Size of inner rectangle as a Point (x,y).
149      */
calculateMinZoom(Point outer, Point inner)150     public static float calculateMinZoom(Point outer, Point inner) {
151         float minZoom;
152         if (inner.x / (float) inner.y > outer.x / (float) outer.y) {
153             minZoom = inner.x / (float) outer.x;
154         } else {
155             minZoom = inner.y / (float) outer.y;
156         }
157         return minZoom;
158     }
159 
160 
161     /**
162      * Calculates the center position of a wallpaper of the given size, based on a "crop surface"
163      * (with extra width to account for parallax) superimposed on the screen. Trying to show as
164      * much of the wallpaper as possible on the crop surface and align screen to crop surface such
165      * that the centered wallpaper matches what would be seen by the user in the left-most home
166      * screen.
167      *
168      * @param wallpaperSize full size of the wallpaper
169      * @param visibleWallpaperRect visible area available for the wallpaper
170      * @return a point corresponding to the position of wallpaper that should be in the center
171      *      of the screen.
172      */
calculateDefaultCenter(Context context, Point wallpaperSize, Rect visibleWallpaperRect)173     public static PointF calculateDefaultCenter(Context context, Point wallpaperSize,
174             Rect visibleWallpaperRect) {
175 
176         WallpaperCropUtils.adjustCurrentWallpaperCropRect(context, wallpaperSize,
177                 visibleWallpaperRect);
178 
179         return new PointF(visibleWallpaperRect.centerX(),
180                 visibleWallpaperRect.centerY());
181     }
182 
183     /**
184      * Calculates the rectangle to crop a wallpaper of the given size, and considering the given
185      * scrollX and scrollY offsets
186      * @param wallpaperZoom zoom applied to the raw wallpaper image
187      * @param wallpaperSize full ("raw") wallpaper size
188      * @param defaultCropSurfaceSize @see #getDefaultCropSurfaceSize(Resources, Display)
189      * @param targetHostSize target size where the wallpaper will be rendered (eg, the display size)
190      * @param scrollX x-axis offset the cropping area from the wallpaper's 0,0 position
191      * @param scrollY y-axis offset the cropping area from the wallpaper's 0,0 position
192      * @return a Rect representing the area of the wallpaper to crop.
193      */
calculateCropRect(Context context, float wallpaperZoom, Point wallpaperSize, Point defaultCropSurfaceSize, Point targetHostSize, int scrollX, int scrollY, boolean cropExtraWidth)194     public static Rect calculateCropRect(Context context, float wallpaperZoom, Point wallpaperSize,
195             Point defaultCropSurfaceSize, Point targetHostSize, int scrollX, int scrollY,
196             boolean cropExtraWidth) {
197         BaseFlags flags = InjectorProvider.getInjector().getFlags();
198         boolean isMultiCropEnabled = flags.isMultiCropEnabled();
199         // Calculate Rect of wallpaper in physical pixel terms (i.e., scaled to current zoom).
200         int scaledWallpaperWidth = Math.round(wallpaperSize.x * wallpaperZoom);
201         int scaledWallpaperHeight = Math.round(wallpaperSize.y * wallpaperZoom);
202         Rect scaledWallpaperRect = new Rect();
203         scaledWallpaperRect.set(0, 0, scaledWallpaperWidth, scaledWallpaperHeight);
204 
205         // Crop rect should start off as the visible screen and then include extra width and height
206         // if available within wallpaper at the current zoom.
207         Rect cropRect = new Rect(scrollX, scrollY, scrollX + targetHostSize.x,
208                 scrollY + targetHostSize.y);
209 
210         int extraWidth = defaultCropSurfaceSize.x - targetHostSize.x;
211         int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - targetHostSize.y) / 2f);
212 
213         if (cropExtraWidth) {
214             // Try to increase size of screenRect to include extra width depending on the layout
215             // direction.
216             if (RtlUtils.isRtl(context)) {
217                 cropRect.left = Math.max(cropRect.left - extraWidth, scaledWallpaperRect.left);
218             } else {
219                 cropRect.right = Math.min(cropRect.right + extraWidth, scaledWallpaperRect.right);
220             }
221         }
222 
223         // Try to increase the size of the cropRect to to include extra height.
224         int availableExtraHeightTop = cropRect.top - Math.max(
225                 scaledWallpaperRect.top,
226                 cropRect.top - extraHeightTopAndBottom);
227         int availableExtraHeightBottom = Math.min(
228                 scaledWallpaperRect.bottom,
229                 cropRect.bottom + extraHeightTopAndBottom) - cropRect.bottom;
230         if (isMultiCropEnabled) {
231             // availableExtraHeightBottom shouldn't be negative, but it could happen if wallpaper
232             // zoom < 1, in that case it should be zero. Safe guard availableExtraHeightTop as well.
233             // Looks like a bug, but we always have > 1 zoom for existing preview, so flag guard
234             // this block.
235             availableExtraHeightTop = Math.max(availableExtraHeightTop, 0);
236             availableExtraHeightBottom = Math.max(availableExtraHeightBottom, 0);
237         }
238 
239         int availableExtraHeightTopAndBottom =
240                 Math.min(availableExtraHeightTop, availableExtraHeightBottom);
241         cropRect.top -= availableExtraHeightTopAndBottom;
242         cropRect.bottom += availableExtraHeightTopAndBottom;
243 
244         return cropRect;
245     }
246 
247     /**
248      * Calculates the center area of the outer rectangle which is visible in the inner rectangle
249      * after applying the minimum zoom.
250      *
251      * @param outer the size of outer rectangle as a Point (x,y).
252      * @param inner the size of inner rectangle as a Point (x,y).
253      */
calculateVisibleRect(Point outer, Point inner)254     public static Rect calculateVisibleRect(Point outer, Point inner) {
255         PointF visibleRectCenter = new PointF(outer.x / 2f, outer.y / 2f);
256         if (inner.x / (float) inner.y > outer.x / (float) outer.y) {
257             float minZoom = inner.x / (float) outer.x;
258             float visibleRectHeight = inner.y / minZoom;
259             return new Rect(0, (int) (visibleRectCenter.y - visibleRectHeight / 2),
260                     outer.x, (int) (visibleRectCenter.y + visibleRectHeight / 2));
261         } else {
262             float minZoom = inner.y / (float) outer.y;
263             float visibleRectWidth = inner.x / minZoom;
264             return new Rect((int) (visibleRectCenter.x - visibleRectWidth / 2),
265                     0, (int) (visibleRectCenter.x + visibleRectWidth / 2), outer.y);
266         }
267     }
268 
adjustCurrentWallpaperCropRect(Context context, Point assetDimensions, Rect cropRect)269     public static void adjustCurrentWallpaperCropRect(Context context, Point assetDimensions,
270             Rect cropRect) {
271         adjustCropRect(context, cropRect, true /* zoomIn */);
272     }
273 
274     /** Adjust the crop rect by zooming in with systemWallpaperMaxScale. */
adjustCropRect(Context context, Rect cropRect, boolean zoomIn)275     public static void adjustCropRect(Context context, Rect cropRect, boolean zoomIn) {
276         float centerX = cropRect.centerX();
277         float centerY = cropRect.centerY();
278         float width = cropRect.width();
279         float height = cropRect.height();
280         float systemWallpaperMaxScale = getSystemWallpaperMaximumScale(context);
281         float scale = zoomIn ? systemWallpaperMaxScale : 1.0f / systemWallpaperMaxScale;
282 
283         // Adjust the rect according to the system wallpaper's maximum scale.
284         int left = (int) (centerX - (width / 2) / scale);
285         int top = (int) (centerY - (height / 2) / scale);
286         int right = (int) (centerX + (width / 2) / scale);
287         int bottom = (int) (centerY + (height / 2) / scale);
288         cropRect.set(left, top, right, bottom);
289     }
290 
291     /** Adjust the given Point, representing a size by  systemWallpaperMaxScale. */
scaleSize(Context context, Point size)292     public static void scaleSize(Context context, Point size) {
293         float systemWallpaperMaxScale = getSystemWallpaperMaximumScale(context);
294         size.set((int) (size.x * systemWallpaperMaxScale),
295                 (int) (size.y * systemWallpaperMaxScale));
296     }
297     /**
298      * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms
299      * (i.e., scaled to current zoom) when the wallpaper is laid on a fullscreen view.
300      */
calculateCropRect(Context context, Display display, Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom)301     public static Rect calculateCropRect(Context context, Display display, Point rawWallpaperSize,
302             Rect visibleRawWallpaperRect, float wallpaperZoom) {
303         Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(display);
304         Point defaultCropSize = WallpaperCropUtils.getDefaultCropSurfaceSize(
305                 context.getResources(), display);
306         return calculateCropRect(context, screenSize, defaultCropSize, rawWallpaperSize,
307                 visibleRawWallpaperRect, wallpaperZoom);
308     }
309 
310     /**
311      * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms
312      * (i.e., scaled to current zoom).
313      *
314      * @param hostViewSize            the size of the view hosting the wallpaper as a Point (x,y).
315      * @param cropSize                the default size of the crop as a Point (x,y).
316      * @param rawWallpaperSize        the size of the raw wallpaper as a Point (x,y).
317      * @param visibleRawWallpaperRect the area of the raw wallpaper which is expected to see.
318      * @param wallpaperZoom           the factor which is used to scale the raw wallpaper.
319      * @param cropExtraWidth          true to crop extra wallpaper width for panel sliding.
320      */
calculateCropRect(Context context, Point hostViewSize, Point cropSize, Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom, boolean cropExtraWidth)321     public static Rect calculateCropRect(Context context, Point hostViewSize, Point cropSize,
322             Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom,
323             boolean cropExtraWidth) {
324         int scrollX = (int) (visibleRawWallpaperRect.left * wallpaperZoom);
325         int scrollY = (int) (visibleRawWallpaperRect.top * wallpaperZoom);
326 
327         return calculateCropRect(context, wallpaperZoom, rawWallpaperSize, cropSize, hostViewSize,
328                 scrollX, scrollY, cropExtraWidth);
329     }
330 
331     /**
332      * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms
333      * (i.e., scaled to current zoom).
334      *
335      * @param hostViewSize            the size of the view hosting the wallpaper as a Point (x,y).
336      * @param cropSize                the default size of the crop as a Point (x,y).
337      * @param rawWallpaperSize        the size of the raw wallpaper as a Point (x,y).
338      * @param visibleRawWallpaperRect the area of the raw wallpaper which is expected to see.
339      * @param wallpaperZoom           the factor which is used to scale the raw wallpaper.
340      */
calculateCropRect(Context context, Point hostViewSize, Point cropSize, Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom)341     public static Rect calculateCropRect(Context context, Point hostViewSize, Point cropSize,
342             Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom) {
343         return calculateCropRect(context, hostViewSize, cropSize, rawWallpaperSize,
344                 visibleRawWallpaperRect, wallpaperZoom, /* cropExtraWidth= */ true);
345     }
346 
347     /**
348      * Resize the wallpaper size so it's new size fits in a outWidth by outHeight rectangle.
349      *
350      * @param wallpaperSize Rectangle with the current wallpaper size. It will be resized.
351      * @param outWidth      the width of the rectangle in which the wallpaperSize needs to fit.
352      * @param outHeight     the height of the rectangle in which the wallpaperSize needs to fit.
353      */
fitToSize(Rect wallpaperSize, int outWidth, int outHeight)354     public static void fitToSize(Rect wallpaperSize, int outWidth, int outHeight) {
355         if (wallpaperSize.isEmpty()) {
356             return;
357         }
358         float maxSizeOut = Math.max(outWidth, outHeight);
359         float maxSizeIn = Math.max(wallpaperSize.width(), wallpaperSize.height());
360         float scale = maxSizeOut / maxSizeIn;
361 
362         // Scale the wallpaper size
363         if (scale != 1.0f) {
364             wallpaperSize.left = (int) (wallpaperSize.left * scale + 0.5f);
365             wallpaperSize.top = (int) (wallpaperSize.top * scale + 0.5f);
366             wallpaperSize.right = (int) (wallpaperSize.right * scale + 0.5f);
367             wallpaperSize.bottom = (int) (wallpaperSize.bottom * scale + 0.5f);
368         }
369     }
370 
371     /**
372      * Get the system wallpaper's maximum scale value.
373      */
getSystemWallpaperMaximumScale(Context context)374     public static float getSystemWallpaperMaximumScale(Context context) {
375         return context.getResources()
376                 .getFloat(Resources.getSystem().getIdentifier(
377                         /* name= */ "config_wallpaperMaxScale",
378                         /* defType= */ "dimen",
379                         /* defPackage= */ "android"));
380     }
381 
382     /**
383      * Gets the scale of screen size and crop rect real size
384      *
385      * @param wallpaperScale The scale of crop rect and real size rect
386      * @param cropRect The area wallpaper cropped
387      * @param screenWidth  The width of screen size
388      * @param screenHeight The height of screen size
389      */
getScaleOfScreenResolution(float wallpaperScale, Rect cropRect, int screenWidth, int screenHeight)390     public static float getScaleOfScreenResolution(float wallpaperScale, Rect cropRect,
391             int screenWidth, int screenHeight) {
392         int rectRealWidth = Math.round((float) cropRect.width() / wallpaperScale);
393         int rectRealHeight = Math.round((float) cropRect.height() / wallpaperScale);
394         int cropWidth = cropRect.width();
395         int cropHeight = cropRect.height();
396         // Not scale with screen resolution because cropRect is bigger than screen size.
397         if (cropWidth >= screenWidth || cropHeight >= screenHeight) {
398             return 1;
399         }
400 
401         int newWidth = screenWidth;
402         int newHeight = screenHeight;
403         // Screen size is bigger than crop real size so we only need enlarge to real size
404         if (newWidth > rectRealWidth || newHeight > rectRealHeight) {
405             newWidth = rectRealWidth;
406             newHeight = rectRealWidth;
407         }
408         float screenScale = Math.min((float) newWidth / cropWidth, (float) newHeight / cropHeight);
409 
410         // screenScale < 1 means our real crop size is smaller than crop size it should be.
411         // So we do nothing in this case, otherwise it'll cause wallpaper smaller than we expected.
412         if (screenScale < 1) {
413             return 1;
414         }
415         return screenScale;
416     }
417 }
418