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 import android.view.View;
27 
28 /**
29  * Static utility methods for wallpaper cropping operations.
30  */
31 public final class WallpaperCropUtils {
32 
33     private static final float WALLPAPER_SCREENS_SPAN = 2f;
34     private static final float ASPECT_RATIO_LANDSCAPE = 16 / 10f;
35     private static final float ASPECT_RATIO_PORTRAIT = 10 / 16f;
36     private static final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.2f;
37     private static final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.5f;
38 
39     // Suppress default constructor for noninstantiability.
WallpaperCropUtils()40     private WallpaperCropUtils() {
41         throw new AssertionError();
42     }
43 
44     /**
45      * Calculates parallax travel (i.e., extra width) for a screen with the given resolution.
46      */
wallpaperTravelToScreenWidthRatio(int width, int height)47     public static float wallpaperTravelToScreenWidthRatio(int width, int height) {
48         float aspectRatio = width / (float) height;
49 
50         // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.2 * screen width
51         // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.5 * screen width
52         // We will use these two data points to extrapolate how much the wallpaper parallax effect
53         // to span (i.e., travel) at any aspect ratio. We use the following two linear formulas,
54         // where
55         // the coefficient on x is the aspect ratio (width/height):
56         //   (16/10)x + y = 1.2
57         //   (10/16)x + y = 1.5
58         // We solve for x and y and end up with a final formula:
59         float x = (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE
60                 - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT)
61                 / (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
62         float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
63         return x * aspectRatio + y;
64     }
65 
66     /**
67      * Calculates ideal crop surface size for a device such that there is room for parallax in both
68      * landscape and portrait screen orientations.
69      */
getDefaultCropSurfaceSize(Resources resources, Display display)70     public static Point getDefaultCropSurfaceSize(Resources resources, Display display) {
71         Point minDims = new Point();
72         Point maxDims = new Point();
73         display.getCurrentSizeRange(minDims, maxDims);
74 
75         int maxDim = Math.max(maxDims.x, maxDims.y);
76         int minDim = Math.max(minDims.x, minDims.y);
77 
78         if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
79             Point realSize = new Point();
80             display.getRealSize(realSize);
81             maxDim = Math.max(realSize.x, realSize.y);
82             minDim = Math.min(realSize.x, realSize.y);
83         }
84 
85         final int defaultWidth, defaultHeight;
86         if (resources.getConfiguration().smallestScreenWidthDp >= 720) {
87             defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
88             defaultHeight = maxDim;
89         } else {
90             defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
91             defaultHeight = maxDim;
92         }
93 
94         return new Point(defaultWidth, defaultHeight);
95     }
96 
97     /**
98      * Calculates the relative position between an outer and inner rectangle when the outer one is
99      * center-cropped to the inner one.
100      *
101      * @param outer      Size of outer rectangle as a Point (x,y).
102      * @param inner      Size of inner rectangle as a Point (x,y).
103      * @param alignStart Whether the inner rectangle should be aligned to the start of the layout
104      *                   with the outer one and ignore horizontal centering.
105      * @param isRtl      Whether the layout direction is RTL (or false for LTR).
106      * @return Position relative to the top left corner of the outer rectangle, where the size of
107      * each rectangle is represented by Points, in coordinates (x,y) relative to the outer rectangle
108      * where the top left corner is (0,0)
109      * @throws IllegalArgumentException if inner rectangle is not contained within outer rectangle
110      *                                  which would return a position with at least one negative
111      *                                  coordinate.
112      */
calculateCenterPosition(Point outer, Point inner, boolean alignStart, boolean isRtl)113     public static Point calculateCenterPosition(Point outer, Point inner, boolean alignStart,
114             boolean isRtl) {
115         if (inner.x > outer.x || inner.y > outer.y) {
116             throw new IllegalArgumentException("Inner rectangle " + inner + " should be contained"
117                     + " completely within the outer rectangle " + outer + ".");
118         }
119 
120         Point relativePosition = new Point();
121 
122         if (alignStart) {
123             relativePosition.x = isRtl ? outer.x - inner.x : 0;
124         } else {
125             relativePosition.x = Math.round((outer.x - inner.x) / 2f);
126         }
127         relativePosition.y = Math.round((outer.y - inner.y) / 2f);
128 
129         return relativePosition;
130     }
131 
132     /**
133      * Calculates the minimum zoom such that the maximum surface area of the outer rectangle is
134      * visible within the inner rectangle.
135      *
136      * @param outer Size of outer rectangle as a Point (x,y).
137      * @param inner Size of inner rectangle as a Point (x,y).
138      */
calculateMinZoom(Point outer, Point inner)139     public static float calculateMinZoom(Point outer, Point inner) {
140         float minZoom;
141         if (inner.x / (float) inner.y > outer.x / (float) outer.y) {
142             minZoom = inner.x / (float) outer.x;
143         } else {
144             minZoom = inner.y / (float) outer.y;
145         }
146         return minZoom;
147     }
148 
149     /**
150      * Calculates the center area of the outer rectangle which is visible in the inner rectangle
151      * after applying the minimum zoom.
152      *
153      * @param outer the size of outer rectangle as a Point (x,y).
154      * @param inner the size of inner rectangle as a Point (x,y).
155      */
calculateVisibleRect(Point outer, Point inner)156     public static Rect calculateVisibleRect(Point outer, Point inner) {
157         PointF visibleRectCenter = new PointF(outer.x / 2f, outer.y / 2f);
158         if (inner.x / (float) inner.y > outer.x / (float) outer.y) {
159             float minZoom = inner.x / (float) outer.x;
160             float visibleRectHeight = inner.y / minZoom;
161             return new Rect(0, (int) (visibleRectCenter.y - visibleRectHeight / 2),
162                     outer.x, (int) (visibleRectCenter.y + visibleRectHeight / 2));
163         } else {
164             float minZoom = inner.y / (float) outer.y;
165             float visibleRectWidth = inner.x / minZoom;
166             return new Rect((int) (visibleRectCenter.x - visibleRectWidth / 2),
167                     0, (int) (visibleRectCenter.x + visibleRectWidth / 2), outer.y);
168         }
169     }
170 
171     /**
172      * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms
173      * (i.e., scaled to current zoom).
174      *
175      * @param rawWallpaperSize        the size of the raw wallpaper as a Point (x,y).
176      * @param visibleRawWallpaperRect the area of the raw wallpaper which is expected to see.
177      * @param wallpaperZoom           the factor which is used to scale the raw wallpaper.
178      */
calculateCropRect(Context context, Display display, Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom)179     public static Rect calculateCropRect(Context context, Display display, Point rawWallpaperSize,
180                                          Rect visibleRawWallpaperRect, float wallpaperZoom) {
181         int scaledWallpaperWidth = (int) (rawWallpaperSize.x * wallpaperZoom);
182         int scaledWallpaperHeight = (int) (rawWallpaperSize.y * wallpaperZoom);
183         int scrollX = (int) (visibleRawWallpaperRect.left * wallpaperZoom);
184         int scrollY = (int) (visibleRawWallpaperRect.top * wallpaperZoom);
185 
186         visibleRawWallpaperRect.set(0, 0, scaledWallpaperWidth, scaledWallpaperHeight);
187 
188         Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(display);
189         // Crop rect should start off as the visible screen and then include extra width and height
190         // if available within wallpaper at the current zoom.
191         Rect cropRect = new Rect(scrollX, scrollY, scrollX + screenSize.x, scrollY + screenSize.y);
192 
193         Point defaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize(
194                 context.getResources(), display);
195         int extraWidth = defaultCropSurfaceSize.x - screenSize.x;
196         int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - screenSize.y) / 2f);
197 
198         // Try to increase size of screenRect to include extra width depending on the layout
199         // direction.
200         if (isRtl(context)) {
201             cropRect.left = Math.max(cropRect.left - extraWidth, visibleRawWallpaperRect.left);
202         } else {
203             cropRect.right = Math.min(cropRect.right + extraWidth, visibleRawWallpaperRect.right);
204         }
205 
206         // Try to increase the size of the cropRect to to include extra height.
207         int availableExtraHeightTop = cropRect.top - Math.max(
208                 visibleRawWallpaperRect.top,
209                 cropRect.top - extraHeightTopAndBottom);
210         int availableExtraHeightBottom = Math.min(
211                 visibleRawWallpaperRect.bottom,
212                 cropRect.bottom + extraHeightTopAndBottom) - cropRect.bottom;
213 
214         int availableExtraHeightTopAndBottom =
215                 Math.min(availableExtraHeightTop, availableExtraHeightBottom);
216         cropRect.top -= availableExtraHeightTopAndBottom;
217         cropRect.bottom += availableExtraHeightTopAndBottom;
218 
219         return cropRect;
220     }
221 
222     /**
223      * Resize the wallpaper size so it's new size fits in a outWidth by outHeight rectangle.
224      *
225      * @param wallpaperSize Rectangle with the current wallpaper size. It will be resized.
226      * @param outWidth      the width of the rectangle in which the wallpaperSize needs to fit.
227      * @param outHeight     the height of the rectangle in which the wallpaperSize needs to fit.
228      */
fitToSize(Rect wallpaperSize, int outWidth, int outHeight)229     public static void fitToSize(Rect wallpaperSize, int outWidth, int outHeight) {
230         if (wallpaperSize.isEmpty()) {
231             return;
232         }
233         float maxSizeOut = Math.max(outWidth, outHeight);
234         float maxSizeIn = Math.max(wallpaperSize.width(), wallpaperSize.height());
235         float scale = maxSizeOut / maxSizeIn;
236 
237         // Scale the wallpaper size
238         if (scale != 1.0f) {
239             wallpaperSize.left = (int) (wallpaperSize.left * scale + 0.5f);
240             wallpaperSize.top = (int) (wallpaperSize.top * scale + 0.5f);
241             wallpaperSize.right = (int) (wallpaperSize.right * scale + 0.5f);
242             wallpaperSize.bottom = (int) (wallpaperSize.bottom * scale + 0.5f);
243         }
244     }
245 
246     /**
247      * Returns whether layout direction is RTL (or false for LTR). Since native RTL layout support
248      * was added in API 17, returns false for versions lower than 17.
249      */
isRtl(Context context)250     private static boolean isRtl(Context context) {
251         return context.getResources().getConfiguration().getLayoutDirection()
252                 == View.LAYOUT_DIRECTION_RTL;
253     }
254 }
255