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