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 import com.android.systemui.shared.system.WallpaperManagerCompat; 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); 88 } 89 90 /** 91 * Calculates ideal crop surface size for a surface of dimensions maxDim x minDim such that 92 * there is room for parallax in both* landscape and portrait screen orientations. 93 */ calculateCropSurfaceSize(Resources resources, int maxDim, int minDim)94 public static Point calculateCropSurfaceSize(Resources resources, int maxDim, int minDim) { 95 final int defaultWidth, defaultHeight; 96 if (resources.getConfiguration().smallestScreenWidthDp >= 720) { 97 defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); 98 defaultHeight = maxDim; 99 } else { 100 defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); 101 defaultHeight = maxDim; 102 } 103 104 return new Point(defaultWidth, defaultHeight); 105 } 106 107 /** 108 * Calculates the relative position between an outer and inner rectangle when the outer one is 109 * center-cropped to the inner one. 110 * 111 * @param outer Size of outer rectangle as a Point (x,y). 112 * @param inner Size of inner rectangle as a Point (x,y). 113 * @param alignStart Whether the inner rectangle should be aligned to the start of the layout 114 * with the outer one and ignore horizontal centering. 115 * @param isRtl Whether the layout direction is RTL (or false for LTR). 116 * @return Position relative to the top left corner of the outer rectangle, where the size of 117 * each rectangle is represented by Points, in coordinates (x,y) relative to the outer rectangle 118 * where the top left corner is (0,0) 119 * @throws IllegalArgumentException if inner rectangle is not contained within outer rectangle 120 * which would return a position with at least one negative 121 * coordinate. 122 */ calculateCenterPosition(Point outer, Point inner, boolean alignStart, boolean isRtl)123 public static Point calculateCenterPosition(Point outer, Point inner, boolean alignStart, 124 boolean isRtl) { 125 if (inner.x > outer.x || inner.y > outer.y) { 126 throw new IllegalArgumentException("Inner rectangle " + inner + " should be contained" 127 + " completely within the outer rectangle " + outer + "."); 128 } 129 130 Point relativePosition = new Point(); 131 132 if (alignStart) { 133 relativePosition.x = isRtl ? outer.x - inner.x : 0; 134 } else { 135 relativePosition.x = Math.round((outer.x - inner.x) / 2f); 136 } 137 relativePosition.y = Math.round((outer.y - inner.y) / 2f); 138 139 return relativePosition; 140 } 141 142 /** 143 * Calculates the minimum zoom such that the maximum surface area of the outer rectangle is 144 * visible within the inner rectangle. 145 * 146 * @param outer Size of outer rectangle as a Point (x,y). 147 * @param inner Size of inner rectangle as a Point (x,y). 148 */ calculateMinZoom(Point outer, Point inner)149 public static float calculateMinZoom(Point outer, Point inner) { 150 float minZoom; 151 if (inner.x / (float) inner.y > outer.x / (float) outer.y) { 152 minZoom = inner.x / (float) outer.x; 153 } else { 154 minZoom = inner.y / (float) outer.y; 155 } 156 return minZoom; 157 } 158 159 160 /** 161 * Calculates the center position of a wallpaper of the given size, based on a "crop surface" 162 * (with extra width to account for parallax) superimposed on the screen. Trying to show as 163 * much of the wallpaper as possible on the crop surface and align screen to crop surface such 164 * that the centered wallpaper matches what would be seen by the user in the left-most home 165 * screen. 166 * 167 * @param wallpaperSize full size of the wallpaper 168 * @param visibleWallpaperRect visible area available for the wallpaper 169 * @return a point corresponding to the position of wallpaper that should be in the center 170 * of the screen. 171 */ calculateDefaultCenter(Context context, Point wallpaperSize, Rect visibleWallpaperRect)172 public static PointF calculateDefaultCenter(Context context, Point wallpaperSize, 173 Rect visibleWallpaperRect) { 174 175 WallpaperCropUtils.adjustCurrentWallpaperCropRect(context, wallpaperSize, 176 visibleWallpaperRect); 177 178 return new PointF(visibleWallpaperRect.centerX(), 179 visibleWallpaperRect.centerY()); 180 } 181 182 /** 183 * Calculates the rectangle to crop a wallpaper of the given size, and considering the given 184 * scrollX and scrollY offsets 185 * @param wallpaperZoom zoom applied to the raw wallpaper image 186 * @param wallpaperSize full ("raw") wallpaper size 187 * @param defaultCropSurfaceSize @see #getDefaultCropSurfaceSize(Resources, Display) 188 * @param targetHostSize target size where the wallpaper will be rendered (eg, the display size) 189 * @param scrollX x-axis offset the cropping area from the wallpaper's 0,0 position 190 * @param scrollY y-axis offset the cropping area from the wallpaper's 0,0 position 191 * @return a Rect representing the area of the wallpaper to crop. 192 */ calculateCropRect(Context context, float wallpaperZoom, Point wallpaperSize, Point defaultCropSurfaceSize, Point targetHostSize, int scrollX, int scrollY)193 public static Rect calculateCropRect(Context context, float wallpaperZoom, Point wallpaperSize, 194 Point defaultCropSurfaceSize, Point targetHostSize, int scrollX, int scrollY) { 195 // Calculate Rect of wallpaper in physical pixel terms (i.e., scaled to current zoom). 196 int scaledWallpaperWidth = Math.round(wallpaperSize.x * wallpaperZoom); 197 int scaledWallpaperHeight = Math.round(wallpaperSize.y * wallpaperZoom); 198 Rect rect = new Rect(); 199 rect.set(0, 0, scaledWallpaperWidth, scaledWallpaperHeight); 200 201 // Crop rect should start off as the visible screen and then include extra width and height 202 // if available within wallpaper at the current zoom. 203 Rect cropRect = new Rect(scrollX, scrollY, scrollX + targetHostSize.x, 204 scrollY + targetHostSize.y); 205 206 int extraWidth = defaultCropSurfaceSize.x - targetHostSize.x; 207 int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - targetHostSize.y) / 2f); 208 209 // Try to increase size of screenRect to include extra width depending on the layout 210 // direction. 211 if (isRtl(context)) { 212 cropRect.left = Math.max(cropRect.left - extraWidth, rect.left); 213 } else { 214 cropRect.right = Math.min(cropRect.right + extraWidth, rect.right); 215 } 216 217 // Try to increase the size of the cropRect to to include extra height. 218 int availableExtraHeightTop = cropRect.top - Math.max( 219 rect.top, 220 cropRect.top - extraHeightTopAndBottom); 221 int availableExtraHeightBottom = Math.min( 222 rect.bottom, 223 cropRect.bottom + extraHeightTopAndBottom) - cropRect.bottom; 224 225 int availableExtraHeightTopAndBottom = 226 Math.min(availableExtraHeightTop, availableExtraHeightBottom); 227 cropRect.top -= availableExtraHeightTopAndBottom; 228 cropRect.bottom += availableExtraHeightTopAndBottom; 229 230 return cropRect; 231 } 232 233 /** 234 * Calculates the center area of the outer rectangle which is visible in the inner rectangle 235 * after applying the minimum zoom. 236 * 237 * @param outer the size of outer rectangle as a Point (x,y). 238 * @param inner the size of inner rectangle as a Point (x,y). 239 */ calculateVisibleRect(Point outer, Point inner)240 public static Rect calculateVisibleRect(Point outer, Point inner) { 241 PointF visibleRectCenter = new PointF(outer.x / 2f, outer.y / 2f); 242 if (inner.x / (float) inner.y > outer.x / (float) outer.y) { 243 float minZoom = inner.x / (float) outer.x; 244 float visibleRectHeight = inner.y / minZoom; 245 return new Rect(0, (int) (visibleRectCenter.y - visibleRectHeight / 2), 246 outer.x, (int) (visibleRectCenter.y + visibleRectHeight / 2)); 247 } else { 248 float minZoom = inner.y / (float) outer.y; 249 float visibleRectWidth = inner.x / minZoom; 250 return new Rect((int) (visibleRectCenter.x - visibleRectWidth / 2), 251 0, (int) (visibleRectCenter.x + visibleRectWidth / 2), outer.y); 252 } 253 } 254 adjustCurrentWallpaperCropRect(Context context, Point assetDimensions, Rect cropRect)255 public static void adjustCurrentWallpaperCropRect(Context context, Point assetDimensions, 256 Rect cropRect) { 257 adjustCropRect(context, cropRect, true /* zoomIn */); 258 } 259 260 /** Adjust the crop rect by zooming in with systemWallpaperMaxScale. */ adjustCropRect(Context context, Rect cropRect, boolean zoomIn)261 public static void adjustCropRect(Context context, Rect cropRect, boolean zoomIn) { 262 float centerX = cropRect.centerX(); 263 float centerY = cropRect.centerY(); 264 float width = cropRect.width(); 265 float height = cropRect.height(); 266 float systemWallpaperMaxScale = getSystemWallpaperMaximumScale(context); 267 float scale = zoomIn ? systemWallpaperMaxScale : 1.0f / systemWallpaperMaxScale; 268 269 // Adjust the rect according to the system wallpaper's maximum scale. 270 int left = (int) (centerX - (width / 2) / scale); 271 int top = (int) (centerY - (height / 2) / scale); 272 int right = (int) (centerX + (width / 2) / scale); 273 int bottom = (int) (centerY + (height / 2) / scale); 274 cropRect.set(left, top, right, bottom); 275 } 276 277 /** Adjust the given Point, representing a size by systemWallpaperMaxScale. */ scaleSize(Context context, Point size)278 public static void scaleSize(Context context, Point size) { 279 float systemWallpaperMaxScale = getSystemWallpaperMaximumScale(context); 280 size.set((int) (size.x * systemWallpaperMaxScale), 281 (int) (size.y * systemWallpaperMaxScale)); 282 } 283 /** 284 * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms 285 * (i.e., scaled to current zoom) when the wallpaper is laid on a fullscreen view. 286 */ calculateCropRect(Context context, Display display, Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom)287 public static Rect calculateCropRect(Context context, Display display, Point rawWallpaperSize, 288 Rect visibleRawWallpaperRect, float wallpaperZoom) { 289 Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(display); 290 Point defaultCropSize = WallpaperCropUtils.getDefaultCropSurfaceSize( 291 context.getResources(), display); 292 return calculateCropRect(context, screenSize, defaultCropSize, rawWallpaperSize, 293 visibleRawWallpaperRect, wallpaperZoom); 294 } 295 296 /** 297 * Calculates {@link Rect} of the wallpaper which we want to crop to in physical pixel terms 298 * (i.e., scaled to current zoom). 299 * 300 * @param hostViewSize the size of the view hosting the wallpaper as a Point (x,y). 301 * @param cropSize the default size of the crop as a Point (x,y). 302 * @param rawWallpaperSize the size of the raw wallpaper as a Point (x,y). 303 * @param visibleRawWallpaperRect the area of the raw wallpaper which is expected to see. 304 * @param wallpaperZoom the factor which is used to scale the raw wallpaper. 305 */ calculateCropRect(Context context, Point hostViewSize, Point cropSize, Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom)306 public static Rect calculateCropRect(Context context, Point hostViewSize, Point cropSize, 307 Point rawWallpaperSize, Rect visibleRawWallpaperRect, float wallpaperZoom) { 308 int scrollX = (int) (visibleRawWallpaperRect.left * wallpaperZoom); 309 int scrollY = (int) (visibleRawWallpaperRect.top * wallpaperZoom); 310 311 return calculateCropRect(context, wallpaperZoom, rawWallpaperSize, cropSize, hostViewSize, 312 scrollX, scrollY); 313 } 314 315 /** 316 * Resize the wallpaper size so it's new size fits in a outWidth by outHeight rectangle. 317 * 318 * @param wallpaperSize Rectangle with the current wallpaper size. It will be resized. 319 * @param outWidth the width of the rectangle in which the wallpaperSize needs to fit. 320 * @param outHeight the height of the rectangle in which the wallpaperSize needs to fit. 321 */ fitToSize(Rect wallpaperSize, int outWidth, int outHeight)322 public static void fitToSize(Rect wallpaperSize, int outWidth, int outHeight) { 323 if (wallpaperSize.isEmpty()) { 324 return; 325 } 326 float maxSizeOut = Math.max(outWidth, outHeight); 327 float maxSizeIn = Math.max(wallpaperSize.width(), wallpaperSize.height()); 328 float scale = maxSizeOut / maxSizeIn; 329 330 // Scale the wallpaper size 331 if (scale != 1.0f) { 332 wallpaperSize.left = (int) (wallpaperSize.left * scale + 0.5f); 333 wallpaperSize.top = (int) (wallpaperSize.top * scale + 0.5f); 334 wallpaperSize.right = (int) (wallpaperSize.right * scale + 0.5f); 335 wallpaperSize.bottom = (int) (wallpaperSize.bottom * scale + 0.5f); 336 } 337 } 338 339 /** 340 * Get the system wallpaper's maximum scale value. 341 */ getSystemWallpaperMaximumScale(Context context)342 public static float getSystemWallpaperMaximumScale(Context context) { 343 return WallpaperManagerCompat.getWallpaperZoomOutMaxScale(context); 344 } 345 346 /** 347 * Returns whether layout direction is RTL (or false for LTR). Since native RTL layout support 348 * was added in API 17, returns false for versions lower than 17. 349 */ isRtl(Context context)350 public static boolean isRtl(Context context) { 351 return context.getResources().getConfiguration().getLayoutDirection() 352 == View.LAYOUT_DIRECTION_RTL; 353 } 354 } 355