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