From fc21830e6b904c8d156adebb793da0b6a51b7d5c Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 17 Sep 2015 14:59:10 -0700 Subject: Normalizing app icons based on the standard icon guidelines Bug: 18245189 Change-Id: Iaadcddbe3f966733a13b2e1fb60ba09a8b3aef9a --- src/com/android/launcher3/IconCache.java | 2 +- .../android/launcher3/InvariantDeviceProfile.java | 8 +- src/com/android/launcher3/Utilities.java | 20 +- src/com/android/launcher3/util/IconNormalizer.java | 239 +++++++++++++++++++++ 4 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 src/com/android/launcher3/util/IconNormalizer.java (limited to 'src/com/android/launcher3') diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index a77332fd6..2675d2735 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -782,7 +782,7 @@ public class IconCache { } private static final class IconDB extends SQLiteOpenHelper { - private final static int DB_VERSION = 7; + private final static int DB_VERSION = 8; private final static String TABLE_NAME = "icons"; private final static String COLUMN_ROWID = "rowid"; diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index 0d183db37..a91181d5e 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -65,10 +65,10 @@ public class InvariantDeviceProfile { */ public int numFolderRows; public int numFolderColumns; - float iconSize; - int iconBitmapSize; - int fillResIconDpi; - float iconTextSize; + public float iconSize; + public int iconBitmapSize; + public int fillResIconDpi; + public float iconTextSize; /** * Number of icons inside the hotseat area. diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 54d050ff5..0a7028603 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -61,6 +61,7 @@ import android.view.View; import android.widget.Toast; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.IconNormalizer; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -197,12 +198,15 @@ public final class Utilities { } /** - * Returns a bitmap suitable for the all apps view. The icon is badged for {@param user} + * Returns a bitmap suitable for the all apps view. The icon is badged for {@param user}. + * The bitmap is also visually normalized with other icons. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static Bitmap createBadgedIconBitmap( Drawable icon, UserHandleCompat user, Context context) { - Bitmap bitmap = createIconBitmap(icon, context); + float scale = LauncherAppState.isDogfoodBuild() ? + IconNormalizer.getInstance().getScale(icon) : 1; + Bitmap bitmap = createIconBitmap(icon, context, scale); if (Utilities.ATLEAST_LOLLIPOP && user != null && !UserHandleCompat.myUserHandle().equals(user)) { BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); @@ -222,6 +226,13 @@ public final class Utilities { * Returns a bitmap suitable for the all apps view. */ public static Bitmap createIconBitmap(Drawable icon, Context context) { + return createIconBitmap(icon, context, 1.0f /* scale */); + } + + /** + * @param scale the scale to apply before drawing {@param icon} on the canvas + */ + public static Bitmap createIconBitmap(Drawable icon, Context context, float scale) { synchronized (sCanvas) { final int iconBitmapSize = getIconBitmapSize(); @@ -277,7 +288,10 @@ public final class Utilities { sOldBounds.set(icon.getBounds()); icon.setBounds(left, top, left+width, top+height); + canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.scale(scale, scale, textureWidth / 2, textureHeight / 2); icon.draw(canvas); + canvas.restore(); icon.setBounds(sOldBounds); canvas.setBitmap(null); @@ -334,7 +348,7 @@ public final class Utilities { } /** - * Inverse of {@link #getDescendantCoordRelativeToSelf(View, int[])}. + * Inverse of {@link #getDescendantCoordRelativeToParent(View, View, int[], boolean)}. */ public static float mapCoordInSelfToDescendent(View descendant, View root, int[] coord) { diff --git a/src/com/android/launcher3/util/IconNormalizer.java b/src/com/android/launcher3/util/IconNormalizer.java new file mode 100644 index 000000000..001cac0f0 --- /dev/null +++ b/src/com/android/launcher3/util/IconNormalizer.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.drawable.Drawable; + +import com.android.launcher3.LauncherAppState; + +import java.nio.ByteBuffer; + +public class IconNormalizer { + + // Ratio of icon visible area to full icon size for a square shaped icon + private static final float MAX_SQUARE_AREA_FACTOR = 359.0f / 576; + // Ratio of icon visible area to full icon size for a circular shaped icon + private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576; + + private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4; + + // Slope used to calculate icon visible area to full icon size for any generic shaped icon. + private static final float LINEAR_SCALE_SLOPE = + (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT); + + private static final int MIN_VISIBLE_ALPHA = 40; + + private static final Object LOCK = new Object(); + private static IconNormalizer sIconNormalizer; + + private final int mMaxSize; + private final Bitmap mBitmap; + private final Canvas mCanvas; + private final byte[] mPixels; + + // for each y, stores the position of the leftmost x and the rightmost x + private final float[] mLeftBorder; + private final float[] mRightBorder; + + private IconNormalizer() { + // Use twice the icon size as maximum size to avoid scaling down twice. + mMaxSize = LauncherAppState.getInstance().getInvariantDeviceProfile().iconBitmapSize * 2; + mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8); + mCanvas = new Canvas(mBitmap); + mPixels = new byte[mMaxSize * mMaxSize]; + + mLeftBorder = new float[mMaxSize]; + mRightBorder = new float[mMaxSize]; + } + + /** + * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it + * matches the design guidelines for a launcher icon. + * + * We first calculate the convex hull of the visible portion of the icon. + * This hull then compared with the bounding rectangle of the hull to find how closely it + * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an + * ideal solution but it gives satisfactory result without affecting the performance. + * + * This closeness is used to determine the ratio of hull area to the full icon size. + * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR} + */ + public synchronized float getScale(Drawable d) { + int width = d.getIntrinsicWidth(); + int height = d.getIntrinsicHeight(); + if (width <= 0 || height <= 0) { + width = width <= 0 || width > mMaxSize ? mMaxSize : width; + height = height <= 0 || height > mMaxSize ? mMaxSize : height; + } else if (width > mMaxSize || height > mMaxSize) { + int max = Math.max(width, height); + width = mMaxSize * width / max; + height = mMaxSize * height / max; + } + + mBitmap.eraseColor(Color.TRANSPARENT); + d.setBounds(0, 0, width, height); + d.draw(mCanvas); + + ByteBuffer buffer = ByteBuffer.wrap(mPixels); + buffer.rewind(); + mBitmap.copyPixelsToBuffer(buffer); + + // Overall bounds of the visible icon. + int topY = -1; + int bottomY = -1; + int leftX = mMaxSize + 1; + int rightX = -1; + + // Create border by going through all pixels one row at a time and for each row find + // the first and the last non-transparent pixel. Set those values to mLeftBorder and + // mRightBorder and use -1 if there are no visible pixel in the row. + + // buffer position + int index = 0; + // buffer shift after every row, width of buffer = mMaxSize + int rowSizeDiff = mMaxSize - width; + // first and last position for any row. + int firstX, lastX; + + for (int y = 0; y < height; y++) { + firstX = lastX = -1; + for (int x = 0; x < width; x++) { + if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) { + if (firstX == -1) { + firstX = x; + } + lastX = x; + } + index++; + } + index += rowSizeDiff; + + mLeftBorder[y] = firstX; + mRightBorder[y] = lastX; + + // If there is at least one visible pixel, update the overall bounds. + if (firstX != -1) { + bottomY = y; + if (topY == -1) { + topY = y; + } + + leftX = Math.min(leftX, firstX); + rightX = Math.max(rightX, lastX); + } + } + + if (topY == -1 || rightX == -1) { + // No valid pixels found. Do not scale. + return 1; + } + + convertToConvexArray(mLeftBorder, 1, topY, bottomY); + convertToConvexArray(mRightBorder, -1, topY, bottomY); + + // Area of the convex hull + float area = 0; + for (int y = 0; y < height; y++) { + if (mLeftBorder[y] <= -1) { + continue; + } + area += mRightBorder[y] - mLeftBorder[y] + 1; + } + + // Area of the rectangle required to fit the convex hull + float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX); + float hullByRect = area / rectArea; + + float scaleRequired; + if (hullByRect < CIRCLE_AREA_BY_RECT) { + scaleRequired = MAX_CIRCLE_AREA_FACTOR; + } else { + scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect); + } + + float areaScale = area / (width * height); + // Use sqrt of the final ratio as the images is scaled across both width and height. + float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1; + return scale; + } + + /** + * Modifies {@param xCordinates} to represent a convex border. Fills in all missing values + * (except on either ends) with appropriate values. + * @param xCordinates map of x coordinate per y. + * @param direction 1 for left border and -1 for right border. + * @param topY the first Y position (inclusive) with a valid value. + * @param bottomY the last Y position (inclusive) with a valid value. + */ + private static void convertToConvexArray( + float[] xCordinates, int direction, int topY, int bottomY) { + int total = xCordinates.length; + // The tangent at each pixel. + float[] angles = new float[total - 1]; + + int first = topY; // First valid y coordinate + int last = -1; // Last valid y coordinate which didn't have a missing value + + float lastAngle = Float.MAX_VALUE; + + for (int i = topY + 1; i <= bottomY; i++) { + if (xCordinates[i] <= -1) { + continue; + } + int start; + + if (lastAngle == Float.MAX_VALUE) { + start = first; + } else { + float currentAngle = (xCordinates[i] - xCordinates[last]) / (i - last); + start = last; + // If this position creates a concave angle, keep moving up until we find a + // position which creates a convex angle. + if ((currentAngle - lastAngle) * direction < 0) { + while (start > first) { + start --; + currentAngle = (xCordinates[i] - xCordinates[start]) / (i - start); + if ((currentAngle - angles[start]) * direction >= 0) { + break; + } + } + } + } + + // Reset from last check + lastAngle = (xCordinates[i] - xCordinates[start]) / (i - start); + // Update all the points from start. + for (int j = start; j < i; j++) { + angles[j] = lastAngle; + xCordinates[j] = xCordinates[start] + lastAngle * (j - start); + } + last = i; + } + } + + public static IconNormalizer getInstance() { + synchronized (LOCK) { + if (sIconNormalizer == null) { + sIconNormalizer = new IconNormalizer(); + } + } + return sIconNormalizer; + } +} -- cgit v1.2.3