From c1cf75716b21003d334eb3e43a07aecfd0d3a017 Mon Sep 17 00:00:00 2001 From: Hyunyoung Song Date: Sun, 16 Apr 2017 21:32:20 -0700 Subject: [DO NOT MERGE] legacy icon treatment / circle detection Bug: 37357483 Change-Id: I63049ad61ad259f546fcf5077ded0a5f444e4395 --- .../adaptive_icon_drawable_wrapper.xml | 2 +- res/values/colors.xml | 1 + src/com/android/launcher3/IconCache.java | 2 +- .../launcher3/graphics/FixedScaleDrawable.java | 8 +- .../android/launcher3/graphics/IconNormalizer.java | 181 ++++++++++++++++++++- .../android/launcher3/graphics/LauncherIcons.java | 60 ++++++- .../com/android/launcher3/config/FeatureFlags.java | 4 +- 7 files changed, 236 insertions(+), 22 deletions(-) diff --git a/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/res/drawable-v26/adaptive_icon_drawable_wrapper.xml index 7ad8e2c64..2d78b699f 100644 --- a/res/drawable-v26/adaptive_icon_drawable_wrapper.xml +++ b/res/drawable-v26/adaptive_icon_drawable_wrapper.xml @@ -15,7 +15,7 @@ limitations under the License. --> - + diff --git a/res/values/colors.xml b/res/values/colors.xml index 3ce7baae2..f148cf2a5 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -45,5 +45,6 @@ @android:color/tertiary_text_light + #FFFFFF #E0E0E0 diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 924b79be0..2a8397c49 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -767,7 +767,7 @@ public class IconCache { } private static final class IconDB extends SQLiteCacheHelper { - private final static int DB_VERSION = 11; + private final static int DB_VERSION = 12; private final static int RELEASE_VERSION = DB_VERSION + (FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ? 0 : 1); diff --git a/src/com/android/launcher3/graphics/FixedScaleDrawable.java b/src/com/android/launcher3/graphics/FixedScaleDrawable.java index 4be4bd552..7ee3d8002 100644 --- a/src/com/android/launcher3/graphics/FixedScaleDrawable.java +++ b/src/com/android/launcher3/graphics/FixedScaleDrawable.java @@ -19,15 +19,17 @@ public class FixedScaleDrawable extends DrawableWrapper { // TODO b/33553066 use the constant defined in MaskableIconDrawable private static final float LEGACY_ICON_SCALE = .7f * .6667f; + private float mScale; public FixedScaleDrawable() { super(new ColorDrawable()); + mScale = LEGACY_ICON_SCALE; } @Override public void draw(Canvas canvas) { int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); - canvas.scale(LEGACY_ICON_SCALE, LEGACY_ICON_SCALE, + canvas.scale(mScale, mScale, getBounds().exactCenterX(), getBounds().exactCenterY()); super.draw(canvas); canvas.restoreToCount(saveCount); @@ -38,4 +40,8 @@ public class FixedScaleDrawable extends DrawableWrapper { @Override public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { } + + public void setScale(float scale) { + mScale = scale * LEGACY_ICON_SCALE; + } } diff --git a/src/com/android/launcher3/graphics/IconNormalizer.java b/src/com/android/launcher3/graphics/IconNormalizer.java index 70b3dd6c9..13f322e8b 100644 --- a/src/com/android/launcher3/graphics/IconNormalizer.java +++ b/src/com/android/launcher3/graphics/IconNormalizer.java @@ -20,15 +20,31 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.Utilities; +import java.io.File; +import java.io.FileOutputStream; import java.nio.ByteBuffer; +import java.util.Random; public class IconNormalizer { + private static final String TAG = "IconNormalizer"; + private static final boolean DEBUG = false; // Ratio of icon visible area to full icon size for a square shaped icon private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576; // Ratio of icon visible area to full icon size for a circular shaped icon @@ -42,17 +58,36 @@ public class IconNormalizer { private static final int MIN_VISIBLE_ALPHA = 40; + // Shape detection related constants + private static final float BOUND_RATIO_MARGIN = .05f; + private static final float PIXEL_DIFF_PERCENTAGE_THRESHOLD = 0.005f; + private static final float SCALE_NOT_INITIALIZED = 0; + private static final Object LOCK = new Object(); private static IconNormalizer sIconNormalizer; private final int mMaxSize; private final Bitmap mBitmap; + private final Bitmap mBitmapARGB; private final Canvas mCanvas; + private final Paint mPaintMaskShape; + private final Paint mPaintMaskShapeOutline; private final byte[] mPixels; + private final int[] mPixelsARGB; + private float mAdaptiveIconScale; // for each y, stores the position of the leftmost x and the rightmost x private final float[] mLeftBorder; private final float[] mRightBorder; + private final Rect mBounds; + private final Matrix mMatrix; + + private Paint mPaintIcon; + private Canvas mCanvasARGB; + + private File mDir; + private int mFileId; + private Random mRandom; private IconNormalizer(Context context) { // Use twice the icon size as maximum size to avoid scaling down twice. @@ -60,9 +95,121 @@ public class IconNormalizer { mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8); mCanvas = new Canvas(mBitmap); mPixels = new byte[mMaxSize * mMaxSize]; - + mPixelsARGB = new int[mMaxSize * mMaxSize]; mLeftBorder = new float[mMaxSize]; mRightBorder = new float[mMaxSize]; + mBounds = new Rect(); + + // Needed for isShape() method + mBitmapARGB = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ARGB_8888); + mCanvasARGB = new Canvas(mBitmapARGB); + + mPaintIcon = new Paint(); + mPaintIcon.setColor(Color.WHITE); + + mPaintMaskShape = new Paint(); + mPaintMaskShape.setColor(Color.RED); + mPaintMaskShape.setStyle(Paint.Style.FILL); + mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR)); + + mPaintMaskShapeOutline = new Paint(); + mPaintMaskShapeOutline.setStrokeWidth(2 * context.getResources().getDisplayMetrics().density); + mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE); + mPaintMaskShapeOutline.setColor(Color.BLACK); + mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + + mMatrix = new Matrix(); + int[] mPixels = new int[mMaxSize * mMaxSize]; + mAdaptiveIconScale = SCALE_NOT_INITIALIZED; + + mDir = context.getExternalFilesDir(null); + mRandom = new Random(); + } + + /** + * Returns if the shape of the icon is same as the path. + * For this method to work, the shape path bounds should be in [0,1]x[0,1] bounds. + */ + private boolean isShape(Path maskPath) { + // Condition1: + // If width and height of the path not close to a square, then the icon shape is + // not same as the mask shape. + float iconRatio = ((float) mBounds.width()) / mBounds.height(); + if (Math.abs(iconRatio - 1) > BOUND_RATIO_MARGIN) { + if (DEBUG) { + Log.d(TAG, "Not same as mask shape because width != height. " + iconRatio); + } + return false; + } + + // Condition 2: + // Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation + // should generate transparent image, if the actual icon is equivalent to the shape. + mFileId = mRandom.nextInt(); + mBitmapARGB.eraseColor(Color.TRANSPARENT); + mCanvasARGB.drawBitmap(mBitmap, 0, 0, mPaintIcon); + + if (DEBUG) { + final File beforeFile = new File(mDir, "isShape" + mFileId + "_before.png"); + try { + mBitmapARGB.compress(Bitmap.CompressFormat.PNG, 100, + new FileOutputStream(beforeFile)); + } catch (Exception e) {} + } + + // Fit the shape within the icon's bounding box + mMatrix.reset(); + mMatrix.setScale(mBounds.width(), mBounds.height()); + mMatrix.postTranslate(mBounds.left, mBounds.top); + maskPath.transform(mMatrix); + + // XOR operation + mCanvasARGB.drawPath(maskPath, mPaintMaskShape); + + // DST_OUT operation around the mask path outline + mCanvasARGB.drawPath(maskPath, mPaintMaskShapeOutline); + + boolean isTrans = isTransparentBitmap(mBitmapARGB); + if (DEBUG) { + final File afterFile = new File(mDir, "isShape" + mFileId + "_after_" + isTrans + ".png"); + try { + mBitmapARGB.compress(Bitmap.CompressFormat.PNG, 100, + new FileOutputStream(afterFile)); + } catch (Exception e) {} + } + + // Check if the result is almost transparent + if (!isTrans) { + if (DEBUG) { + Log.d(TAG, "Not same as mask shape"); + } + return false; + } + return true; + } + + /** + * Used to determine if certain the bitmap is transparent. + */ + private boolean isTransparentBitmap(Bitmap bitmap) { + int w = mBounds.width(); + int h = mBounds.height(); + bitmap.getPixels(mPixelsARGB, 0 /* the first index to write into the array */, + w /* stride */, + mBounds.left, mBounds.top, + w, h); + int sum = 0; + for (int i = 0; i < w * h; i++) { + if(Color.alpha(mPixelsARGB[i]) > MIN_VISIBLE_ALPHA) { + sum++; + } + } + float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height()); + boolean transparentImage = percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD; + if (DEBUG) { + Log.d(TAG, "Total # pixel that is different (id="+ mFileId + "):" + percentageDiffPixels + "="+ sum + "/" + mBounds.width() * mBounds.height()); + } + return transparentImage; } /** @@ -79,7 +226,15 @@ public class IconNormalizer { * * @param outBounds optional rect to receive the fraction distance from each edge. */ - public synchronized float getScale(Drawable d, RectF outBounds) { + public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds, + @Nullable Path path, @Nullable boolean[] outMaskShape) { + if (Utilities.isAtLeastO() && d instanceof AdaptiveIconDrawable && + mAdaptiveIconScale != SCALE_NOT_INITIALIZED) { + if (outBounds != null) { + outBounds.set(mBounds); + } + return mAdaptiveIconScale; + } int width = d.getIntrinsicWidth(); int height = d.getIntrinsicHeight(); if (width <= 0 || height <= 0) { @@ -169,20 +324,30 @@ public class IconNormalizer { if (hullByRect < CIRCLE_AREA_BY_RECT) { scaleRequired = MAX_CIRCLE_AREA_FACTOR; } else { - scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect); + scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect); } + mBounds.left = leftX; + mBounds.right = rightX; - if (outBounds != null) { - outBounds.left = ((float) leftX) / width; - outBounds.right = 1 - ((float) rightX) / width; + mBounds.top = topY; + mBounds.bottom = bottomY; - outBounds.top = ((float) topY) / height; - outBounds.bottom = 1 - ((float) bottomY) / height; + if (outBounds != null) { + outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top), + 1 - ((float) mBounds.right) / width, + 1 - ((float) mBounds.bottom) / height); } + if (outMaskShape != null && outMaskShape.length > 0) { + outMaskShape[0] = isShape(path); + } 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; + if (Utilities.isAtLeastO() && d instanceof AdaptiveIconDrawable && + mAdaptiveIconScale == SCALE_NOT_INITIALIZED) { + mAdaptiveIconScale = scale; + } return scale; } diff --git a/src/com/android/launcher3/graphics/LauncherIcons.java b/src/com/android/launcher3/graphics/LauncherIcons.java index 2d987cc4a..f652a5c15 100644 --- a/src/com/android/launcher3/graphics/LauncherIcons.java +++ b/src/com/android/launcher3/graphics/LauncherIcons.java @@ -95,8 +95,29 @@ public class LauncherIcons { */ public static Bitmap createBadgedIconBitmap( Drawable icon, UserHandle user, Context context) { - float scale = FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ? - 1 : IconNormalizer.getInstance(context).getScale(icon, null); + + IconNormalizer normalizer; + float scale = 1f; + if (!FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION) { + normalizer = IconNormalizer.getInstance(context); + if (Utilities.isAtLeastO()) { + boolean[] outShape = new boolean[1]; + AdaptiveIconDrawable dr = (AdaptiveIconDrawable) + context.getDrawable(R.drawable.adaptive_icon_drawable_wrapper).mutate(); + dr.setBounds(0, 0, 1, 1); + scale = normalizer.getScale(icon, null, dr.getIconMask(), outShape); + if (FeatureFlags.LEGACY_ICON_TREATMENT && + !outShape[0]){ + Drawable wrappedIcon = wrapToAdaptiveIconDrawable(context, icon, scale); + if (wrappedIcon != icon) { + icon = wrappedIcon; + scale = normalizer.getScale(icon, null, null, null); + } + } + } else { + scale = normalizer.getScale(icon, null, null, null); + } + } Bitmap bitmap = createIconBitmap(icon, context, scale); if (FeatureFlags.ADAPTIVE_ICON_SHADOW && Utilities.isAtLeastO() && icon instanceof AdaptiveIconDrawable) { @@ -129,8 +150,29 @@ public class LauncherIcons { */ public static Bitmap createScaledBitmapWithoutShadow(Drawable icon, Context context) { RectF iconBounds = new RectF(); - float scale = FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ? - 1 : IconNormalizer.getInstance(context).getScale(icon, iconBounds); + IconNormalizer normalizer; + float scale = 1f; + if (!FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION) { + normalizer = IconNormalizer.getInstance(context); + if (Utilities.isAtLeastO()) { + boolean[] outShape = new boolean[1]; + AdaptiveIconDrawable dr = (AdaptiveIconDrawable) + context.getDrawable(R.drawable.adaptive_icon_drawable_wrapper).mutate(); + dr.setBounds(0, 0, 1, 1); + scale = normalizer.getScale(icon, iconBounds, dr.getIconMask(), outShape); + if (Utilities.isAtLeastO() && FeatureFlags.LEGACY_ICON_TREATMENT && + !outShape[0]) { + Drawable wrappedIcon = wrapToAdaptiveIconDrawable(context, icon, scale); + if (wrappedIcon != icon) { + icon = wrappedIcon; + scale = normalizer.getScale(icon, iconBounds, null, null); + } + } + } else { + scale = normalizer.getScale(icon, iconBounds, null, null); + } + + } scale = Math.min(scale, ShadowGenerator.getScaleForBounds(iconBounds)); return createIconBitmap(icon, context, scale); } @@ -180,10 +222,8 @@ public class LauncherIcons { * @param scale the scale to apply before drawing {@param icon} on the canvas */ public static Bitmap createIconBitmap(Drawable icon, Context context, float scale) { - icon = wrapToAdaptiveIconDrawable(context, icon); synchronized (sCanvas) { final int iconBitmapSize = LauncherAppState.getIDP(context).iconBitmapSize; - int width = iconBitmapSize; int height = iconBitmapSize; @@ -242,7 +282,7 @@ public class LauncherIcons { * shrink the legacy icon and set it as foreground. Use color drawable as background to * create AdaptiveIconDrawable. */ - static Drawable wrapToAdaptiveIconDrawable(Context context, Drawable drawable) { + static Drawable wrapToAdaptiveIconDrawable(Context context, Drawable drawable, float scale) { if (!(FeatureFlags.LEGACY_ICON_TREATMENT && Utilities.isAtLeastO())) { return drawable; } @@ -252,8 +292,10 @@ public class LauncherIcons { if (!clazz.isAssignableFrom(drawable.getClass())) { Drawable iconWrapper = context.getDrawable(R.drawable.adaptive_icon_drawable_wrapper).mutate(); - ((FixedScaleDrawable) clazz.getMethod("getForeground").invoke(iconWrapper)) - .setDrawable(drawable); + FixedScaleDrawable fsd = ((FixedScaleDrawable) clazz.getMethod("getForeground") + .invoke(iconWrapper)); + fsd.setDrawable(drawable); + fsd.setScale(scale); return iconWrapper; } diff --git a/src_config/com/android/launcher3/config/FeatureFlags.java b/src_config/com/android/launcher3/config/FeatureFlags.java index 80eebece2..4e337a2b8 100644 --- a/src_config/com/android/launcher3/config/FeatureFlags.java +++ b/src_config/com/android/launcher3/config/FeatureFlags.java @@ -45,8 +45,8 @@ public final class FeatureFlags { public static final boolean LIGHT_STATUS_BAR = false; // When enabled icons are badged with the number of notifications associated with that app. public static final boolean BADGE_ICONS = true; - // When enabled, icons not supporting {@link MaskableIconDrawable} will be wrapped in this class. - public static final boolean LEGACY_ICON_TREATMENT = false; + // When enabled, icons not supporting {@link AdaptiveIconDrawable} will be wrapped in this class. + public static final boolean LEGACY_ICON_TREATMENT = true; // When enabled, adaptive icons would have shadows baked when being stored to icon cache. public static final boolean ADAPTIVE_ICON_SHADOW = true; // When enabled, app discovery will be enabled if service is implemented -- cgit v1.2.3