1/* 2 * Copyright (C) 2015 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 17package com.android.launcher3.util; 18 19import android.graphics.Bitmap; 20import android.graphics.Canvas; 21import android.graphics.Color; 22import android.graphics.RectF; 23import android.graphics.drawable.Drawable; 24 25import com.android.launcher3.LauncherAppState; 26 27import java.nio.ByteBuffer; 28 29public class IconNormalizer { 30 31 // Ratio of icon visible area to full icon size for a square shaped icon 32 private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576; 33 // Ratio of icon visible area to full icon size for a circular shaped icon 34 private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576; 35 36 private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4; 37 38 // Slope used to calculate icon visible area to full icon size for any generic shaped icon. 39 private static final float LINEAR_SCALE_SLOPE = 40 (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT); 41 42 private static final int MIN_VISIBLE_ALPHA = 40; 43 44 private static final Object LOCK = new Object(); 45 private static IconNormalizer sIconNormalizer; 46 47 private final int mMaxSize; 48 private final Bitmap mBitmap; 49 private final Canvas mCanvas; 50 private final byte[] mPixels; 51 52 // for each y, stores the position of the leftmost x and the rightmost x 53 private final float[] mLeftBorder; 54 private final float[] mRightBorder; 55 56 private IconNormalizer() { 57 // Use twice the icon size as maximum size to avoid scaling down twice. 58 mMaxSize = LauncherAppState.getInstance().getInvariantDeviceProfile().iconBitmapSize * 2; 59 mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8); 60 mCanvas = new Canvas(mBitmap); 61 mPixels = new byte[mMaxSize * mMaxSize]; 62 63 mLeftBorder = new float[mMaxSize]; 64 mRightBorder = new float[mMaxSize]; 65 } 66 67 /** 68 * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it 69 * matches the design guidelines for a launcher icon. 70 * 71 * We first calculate the convex hull of the visible portion of the icon. 72 * This hull then compared with the bounding rectangle of the hull to find how closely it 73 * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an 74 * ideal solution but it gives satisfactory result without affecting the performance. 75 * 76 * This closeness is used to determine the ratio of hull area to the full icon size. 77 * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR} 78 * 79 * @param outBounds optional rect to receive the fraction distance from each edge. 80 */ 81 public synchronized float getScale(Drawable d, RectF outBounds) { 82 int width = d.getIntrinsicWidth(); 83 int height = d.getIntrinsicHeight(); 84 if (width <= 0 || height <= 0) { 85 width = width <= 0 || width > mMaxSize ? mMaxSize : width; 86 height = height <= 0 || height > mMaxSize ? mMaxSize : height; 87 } else if (width > mMaxSize || height > mMaxSize) { 88 int max = Math.max(width, height); 89 width = mMaxSize * width / max; 90 height = mMaxSize * height / max; 91 } 92 93 mBitmap.eraseColor(Color.TRANSPARENT); 94 d.setBounds(0, 0, width, height); 95 d.draw(mCanvas); 96 97 ByteBuffer buffer = ByteBuffer.wrap(mPixels); 98 buffer.rewind(); 99 mBitmap.copyPixelsToBuffer(buffer); 100 101 // Overall bounds of the visible icon. 102 int topY = -1; 103 int bottomY = -1; 104 int leftX = mMaxSize + 1; 105 int rightX = -1; 106 107 // Create border by going through all pixels one row at a time and for each row find 108 // the first and the last non-transparent pixel. Set those values to mLeftBorder and 109 // mRightBorder and use -1 if there are no visible pixel in the row. 110 111 // buffer position 112 int index = 0; 113 // buffer shift after every row, width of buffer = mMaxSize 114 int rowSizeDiff = mMaxSize - width; 115 // first and last position for any row. 116 int firstX, lastX; 117 118 for (int y = 0; y < height; y++) { 119 firstX = lastX = -1; 120 for (int x = 0; x < width; x++) { 121 if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) { 122 if (firstX == -1) { 123 firstX = x; 124 } 125 lastX = x; 126 } 127 index++; 128 } 129 index += rowSizeDiff; 130 131 mLeftBorder[y] = firstX; 132 mRightBorder[y] = lastX; 133 134 // If there is at least one visible pixel, update the overall bounds. 135 if (firstX != -1) { 136 bottomY = y; 137 if (topY == -1) { 138 topY = y; 139 } 140 141 leftX = Math.min(leftX, firstX); 142 rightX = Math.max(rightX, lastX); 143 } 144 } 145 146 if (topY == -1 || rightX == -1) { 147 // No valid pixels found. Do not scale. 148 return 1; 149 } 150 151 convertToConvexArray(mLeftBorder, 1, topY, bottomY); 152 convertToConvexArray(mRightBorder, -1, topY, bottomY); 153 154 // Area of the convex hull 155 float area = 0; 156 for (int y = 0; y < height; y++) { 157 if (mLeftBorder[y] <= -1) { 158 continue; 159 } 160 area += mRightBorder[y] - mLeftBorder[y] + 1; 161 } 162 163 // Area of the rectangle required to fit the convex hull 164 float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX); 165 float hullByRect = area / rectArea; 166 167 float scaleRequired; 168 if (hullByRect < CIRCLE_AREA_BY_RECT) { 169 scaleRequired = MAX_CIRCLE_AREA_FACTOR; 170 } else { 171 scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect); 172 } 173 174 if (outBounds != null) { 175 outBounds.left = ((float) leftX) / width; 176 outBounds.right = 1 - ((float) rightX) / width; 177 178 outBounds.top = ((float) topY) / height; 179 outBounds.bottom = 1 - ((float) bottomY) / height; 180 } 181 182 float areaScale = area / (width * height); 183 // Use sqrt of the final ratio as the images is scaled across both width and height. 184 float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1; 185 return scale; 186 } 187 188 /** 189 * Modifies {@param xCordinates} to represent a convex border. Fills in all missing values 190 * (except on either ends) with appropriate values. 191 * @param xCordinates map of x coordinate per y. 192 * @param direction 1 for left border and -1 for right border. 193 * @param topY the first Y position (inclusive) with a valid value. 194 * @param bottomY the last Y position (inclusive) with a valid value. 195 */ 196 private static void convertToConvexArray( 197 float[] xCordinates, int direction, int topY, int bottomY) { 198 int total = xCordinates.length; 199 // The tangent at each pixel. 200 float[] angles = new float[total - 1]; 201 202 int first = topY; // First valid y coordinate 203 int last = -1; // Last valid y coordinate which didn't have a missing value 204 205 float lastAngle = Float.MAX_VALUE; 206 207 for (int i = topY + 1; i <= bottomY; i++) { 208 if (xCordinates[i] <= -1) { 209 continue; 210 } 211 int start; 212 213 if (lastAngle == Float.MAX_VALUE) { 214 start = first; 215 } else { 216 float currentAngle = (xCordinates[i] - xCordinates[last]) / (i - last); 217 start = last; 218 // If this position creates a concave angle, keep moving up until we find a 219 // position which creates a convex angle. 220 if ((currentAngle - lastAngle) * direction < 0) { 221 while (start > first) { 222 start --; 223 currentAngle = (xCordinates[i] - xCordinates[start]) / (i - start); 224 if ((currentAngle - angles[start]) * direction >= 0) { 225 break; 226 } 227 } 228 } 229 } 230 231 // Reset from last check 232 lastAngle = (xCordinates[i] - xCordinates[start]) / (i - start); 233 // Update all the points from start. 234 for (int j = start; j < i; j++) { 235 angles[j] = lastAngle; 236 xCordinates[j] = xCordinates[start] + lastAngle * (j - start); 237 } 238 last = i; 239 } 240 } 241 242 public static IconNormalizer getInstance() { 243 synchronized (LOCK) { 244 if (sIconNormalizer == null) { 245 sIconNormalizer = new IconNormalizer(); 246 } 247 } 248 return sIconNormalizer; 249 } 250} 251