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