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