From d954229d1a0985c723c8f6265ffcbed1007264de Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Tue, 6 Dec 2022 13:33:01 -0800 Subject: [PATCH] Adding support for generating manachrome icon from a colored icon Bug: 261625158 Test: Verified on device Change-Id: Ibda922fd2c9c0a856ea02a8e73f43af8573f2450 --- .../launcher3/config/FeatureFlags.java | 5 + .../launcher3/icons/LauncherIcons.java | 17 ++ .../icons/MonochromeIconFactory.java | 180 ++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 src/com/android/launcher3/icons/MonochromeIconFactory.java diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 8363b68e0e..aa7e5d1d98 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -371,6 +371,11 @@ public final class FeatureFlags { "Enable the ability to tap a staged app during split select to launch it in full screen" ); + public static final BooleanFlag ENABLE_FORCED_MONO_ICON = getDebugFlag( + "ENABLE_FORCED_MONO_ICON", false, + "Enable the ability to generate monochromatic icons, if it is not provided by the app" + ); + public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) { diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java index 5508c49410..57fa8a256b 100644 --- a/src/com/android/launcher3/icons/LauncherIcons.java +++ b/src/com/android/launcher3/icons/LauncherIcons.java @@ -16,7 +16,10 @@ package com.android.launcher3.icons; +import static com.android.launcher3.config.FeatureFlags.ENABLE_FORCED_MONO_ICON; + import android.content.Context; +import android.graphics.drawable.Drawable; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.graphics.IconShape; @@ -68,6 +71,8 @@ public class LauncherIcons extends BaseIconFactory implements AutoCloseable { private LauncherIcons next; + private MonochromeIconFactory mMonochromeIconFactory; + protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) { super(context, fillResIconDpi, iconBitmapSize, IconShape.getShape().enableShapeDetection()); mMonoIconEnabled = Themes.isThemedIconEnabled(context); @@ -90,6 +95,18 @@ public class LauncherIcons extends BaseIconFactory implements AutoCloseable { } } + @Override + protected Drawable getMonochromeDrawable(Drawable base) { + Drawable mono = super.getMonochromeDrawable(base); + if (mono != null || !ENABLE_FORCED_MONO_ICON.get()) { + return mono; + } + if (mMonochromeIconFactory == null) { + mMonochromeIconFactory = new MonochromeIconFactory(mIconBitmapSize); + } + return mMonochromeIconFactory.wrap(base); + } + @Override public void close() { recycle(); diff --git a/src/com/android/launcher3/icons/MonochromeIconFactory.java b/src/com/android/launcher3/icons/MonochromeIconFactory.java new file mode 100644 index 0000000000..511dcc736e --- /dev/null +++ b/src/com/android/launcher3/icons/MonochromeIconFactory.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2022 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.icons; + +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import androidx.annotation.WorkerThread; + +import com.android.launcher3.icons.BaseIconFactory.ClippedMonoDrawable; + +import java.nio.ByteBuffer; + +/** + * Utility class to generate monochrome icons version for a given drawable. + */ +@TargetApi(Build.VERSION_CODES.TIRAMISU) +public class MonochromeIconFactory extends Drawable { + + private final Bitmap mFlatBitmap; + private final Canvas mFlatCanvas; + private final Paint mCopyPaint; + + private final Bitmap mAlphaBitmap; + private final Canvas mAlphaCanvas; + private final byte[] mPixels; + + private final int mBitmapSize; + private final int mEdgePixelLength; + + private final Paint mDrawPaint; + private final Rect mSrcRect; + + MonochromeIconFactory(int iconBitmapSize) { + float extraFactor = AdaptiveIconDrawable.getExtraInsetFraction(); + float viewPortScale = 1 / (1 + 2 * extraFactor); + mBitmapSize = Math.round(iconBitmapSize * 2 * viewPortScale); + mPixels = new byte[mBitmapSize * mBitmapSize]; + mEdgePixelLength = mBitmapSize * (mBitmapSize - iconBitmapSize) / 2; + + mFlatBitmap = Bitmap.createBitmap(mBitmapSize, mBitmapSize, Config.ARGB_8888); + mFlatCanvas = new Canvas(mFlatBitmap); + + mAlphaBitmap = Bitmap.createBitmap(mBitmapSize, mBitmapSize, Config.ALPHA_8); + mAlphaCanvas = new Canvas(mAlphaBitmap); + + mDrawPaint = new Paint(FILTER_BITMAP_FLAG); + mDrawPaint.setColor(Color.WHITE); + mSrcRect = new Rect(0, 0, mBitmapSize, mBitmapSize); + + mCopyPaint = new Paint(FILTER_BITMAP_FLAG); + mCopyPaint.setBlendMode(BlendMode.SRC); + + // Crate a color matrix which converts the icon to grayscale and then uses the average + // of RGB components as the alpha component. + ColorMatrix satMatrix = new ColorMatrix(); + satMatrix.setSaturation(0); + float[] vals = satMatrix.getArray(); + vals[15] = vals[16] = vals[17] = .3333f; + vals[18] = vals[19] = 0; + mCopyPaint.setColorFilter(new ColorMatrixColorFilter(vals)); + } + + private void drawDrawable(Drawable drawable) { + if (drawable != null) { + drawable.setBounds(0, 0, mBitmapSize, mBitmapSize); + drawable.draw(mFlatCanvas); + } + } + + /** + * Creates a monochrome version of the provided drawable + */ + @WorkerThread + public Drawable wrap(Drawable icon) { + if (icon instanceof AdaptiveIconDrawable) { + AdaptiveIconDrawable aid = (AdaptiveIconDrawable) icon; + mFlatCanvas.drawColor(Color.BLACK); + drawDrawable(aid.getBackground()); + drawDrawable(aid.getForeground()); + generateMono(); + return new ClippedMonoDrawable(this); + } else { + mFlatCanvas.drawColor(Color.WHITE); + drawDrawable(icon); + generateMono(); + return this; + } + } + + @WorkerThread + private void generateMono() { + mAlphaCanvas.drawBitmap(mFlatBitmap, 0, 0, mCopyPaint); + + // Scale the end points: + ByteBuffer buffer = ByteBuffer.wrap(mPixels); + buffer.rewind(); + mAlphaBitmap.copyPixelsToBuffer(buffer); + + int min = 0xFF; + int max = 0; + for (byte b : mPixels) { + min = Math.min(min, b & 0xFF); + max = Math.max(max, b & 0xFF); + } + + if (min < max) { + // rescale pixels to increase contrast + float range = max - min; + + // In order to check if the colors should be flipped, we just take the average color + // of top and bottom edge which should correspond to be background color. If the edge + // colors have more opacity, we flip the colors; + int sum = 0; + for (int i = 0; i < mEdgePixelLength; i++) { + sum += (mPixels[i] & 0xFF); + sum += (mPixels[mPixels.length - 1 - i] & 0xFF); + } + float edgeAverage = sum / (mEdgePixelLength * 2f); + float edgeMapped = (edgeAverage - min) / range; + boolean flipColor = edgeMapped > .5f; + + for (int i = 0; i < mPixels.length; i++) { + int p = mPixels[i] & 0xFF; + int p2 = Math.round((p - min) * 0xFF / range); + mPixels[i] = flipColor ? (byte) (255 - p2) : (byte) (p2); + } + buffer.rewind(); + mAlphaBitmap.copyPixelsFromBuffer(buffer); + } + } + + @Override + public void draw(Canvas canvas) { + canvas.drawBitmap(mAlphaBitmap, mSrcRect, getBounds(), mDrawPaint); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int i) { + mDrawPaint.setAlpha(i); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mDrawPaint.setColorFilter(colorFilter); + } +}