Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a1325201 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "iconloaderlib: Adjust luminance delta to maintain contrast when...

Merge "iconloaderlib: Adjust luminance delta to maintain contrast when applying monochrome icons." into main
parents 3c1bde99 ef62191e
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@
<resources>
    <color name="themed_icon_color">@android:color/system_accent1_200</color>
    <color name="themed_icon_background_color">@android:color/system_accent2_800</color>
    <color name="themed_icon_adaptive_background_color">@android:color/system_accent1_800</color>
    <color name="themed_badge_icon_color">@android:color/system_accent2_800</color>
    <color name="themed_badge_icon_background_color">@android:color/system_accent1_200</color>
</resources>
+1 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@
<resources>
    <color name="themed_icon_color">@android:color/system_accent1_700</color>
    <color name="themed_icon_background_color">@android:color/system_accent1_100</color>
    <color name="themed_icon_adaptive_background_color">@android:color/system_accent1_500</color>
    <color name="themed_badge_icon_color">@android:color/system_accent1_700</color>
    <color name="themed_badge_icon_background_color">@android:color/system_accent1_100</color>
</resources>
+14 −55
Original line number Diff line number Diff line
@@ -14,11 +14,8 @@
package com.android.launcher3.icons

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.annotation.FloatRange
import androidx.annotation.VisibleForTesting
import androidx.core.graphics.ColorUtils
import kotlin.math.abs

@@ -29,6 +26,9 @@ enum class ComputationType {

    /** Compute the average luminance of a drawable or a bitmap. */
    AVERAGE,

    /** Compute the difference between the min and max luminance of a drawable or a bitmap. */
    SPREAD,
}

/** Wrapper for the color space to use when computing the luminance. */
@@ -142,7 +142,6 @@ class LuminanceComputer(
        val targetColorWrapper = colorToColorWrapper(targetColor)
        val basisColorWrapper = colorToColorWrapper(basisColor)

        val originalTargetLuminance = targetColorWrapper.luminance
        val basisLuminance = basisColorWrapper.luminance

        // The target luminance should be between 0 and 1, so we need to clamp
@@ -162,22 +161,14 @@ class LuminanceComputer(
        return targetColorWrapper
    }

    /**
     * Compute the luminance of a drawable using the selected color space.
     *
     * @param drawable The drawable to compute the luminance of.
     */
    fun computeLuminance(drawable: Drawable): Double {
        val bitmap = createBitmapFromDrawable(drawable)
        return computeLuminance(bitmap)
    }

    /**
     * Compute the luminance of a bitmap using the selected color space.
     *
     * @param bitmap The bitmap to compute the luminance of.
     * @param scale if true, the bitmap is resized to [BITMAP_SAMPLE_SIZE] for color calculation
     */
    fun computeLuminance(bitmap: Bitmap, scale: Boolean = false): Double {
    @JvmOverloads
    fun computeLuminance(bitmap: Bitmap, scale: Boolean = true): Double {
        val bitmapHeight = bitmap.height
        val bitmapWidth = bitmap.width
        if (bitmapHeight == 0 || bitmapWidth == 0) {
@@ -217,42 +208,7 @@ class LuminanceComputer(
        when (computationType) {
            ComputationType.MEDIAN -> return luminances.sorted().median()
            ComputationType.AVERAGE -> return luminances.average()
        }
    }

    private fun scaleBitmap(
        bitmap: Bitmap,
        targetWidth: Int,
        targetHeight: Int,
        filter: Boolean,
    ): Bitmap {
        if (targetWidth <= 0 || targetHeight <= 0) {
            Log.w(TAG, "Invalid dimensions for scaling: $targetWidth x $targetHeight")
            return bitmap
        }
        return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, filter)
    }

    private fun createBitmapFromDrawable(drawable: Drawable): Bitmap {
        val b = Bitmap.createBitmap(BITMAP_SAMPLE_SIZE, BITMAP_SAMPLE_SIZE, Bitmap.Config.ARGB_8888)
        drawable.setBounds(0, 0, BITMAP_SAMPLE_SIZE, BITMAP_SAMPLE_SIZE)
        drawable.draw(Canvas(b))
        return b
    }

    /**
     * Scale the height and width of a bitmap to a maximum size.
     *
     * @param height The height of the bitmap.
     * @param width The width of the bitmap.
     * @return A pair of the scaled height and width.
     */
    @VisibleForTesting
    fun scaleHeightAndWidth(height: Int, width: Int): Pair<Int, Int> {
        if (height > width) {
            return Pair(BITMAP_SAMPLE_SIZE, (width * BITMAP_SAMPLE_SIZE) / height)
        } else {
            return Pair((height * BITMAP_SAMPLE_SIZE) / width, BITMAP_SAMPLE_SIZE)
            ComputationType.SPREAD -> return luminances.max() - luminances.min()
        }
    }

@@ -298,12 +254,12 @@ class LuminanceComputer(
            LuminanceColorSpace.HSL -> {
                val hsl = FloatArray(3)
                ColorUtils.colorToHSL(color, hsl)
                return HslColor(hsl)
                HslColor(hsl)
            }
            LuminanceColorSpace.LAB -> {
                val lab = DoubleArray(3)
                ColorUtils.colorToLAB(color, lab)
                return LabColor(lab)
                LabColor(lab)
            }
        }
    }
@@ -332,10 +288,13 @@ class LuminanceComputer(
        const val DEFAULT_ABSOLUTE_LUMINANCE_DELTA = 0.1

        @JvmStatic
        fun createDefaultLuminanceComputer(): LuminanceComputer {
        @JvmOverloads
        fun createDefaultLuminanceComputer(
            computationType: ComputationType = ComputationType.AVERAGE
        ): LuminanceComputer {
            return LuminanceComputer(
                LuminanceColorSpace.LAB, // Keep this as the default color space
                ComputationType.AVERAGE,
                computationType,
                Options(
                    ensureMinContrast = ENABLED_CONTRAST_ADJUSTMENT,
                    absoluteLuminanceDelta = ENABLED_ABSOLUTE_LUMINANCE_DELTA,
+33 −19
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@ package com.android.launcher3.icons;

import static android.graphics.Paint.FILTER_BITMAP_FLAG;

import static com.android.launcher3.icons.LuminanceComputer.createDefaultLuminanceComputer;

import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
@@ -54,17 +56,17 @@ public class MonochromeIconFactory extends Drawable {
    private final byte[] mPixels;

    private final int mBitmapSize;
    private final int mEdgePixelLength;

    private final Paint mDrawPaint;
    private final Rect mSrcRect;

    private double mLuminanceDiff = Double.NaN;

    public 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);
@@ -110,17 +112,41 @@ public class MonochromeIconFactory extends Drawable {
    @WorkerThread
    public Drawable wrap(AdaptiveIconDrawable icon) {
        mFlatCanvas.drawColor(Color.BLACK);
        drawDrawable(icon.getBackground());
        drawDrawable(icon.getForeground());
        Drawable bg = icon.getBackground();
        Drawable fg = icon.getForeground();
        if (bg != null && fg != null) {
            LuminanceComputer computer = createDefaultLuminanceComputer();
            // Calculate foreground luminance on black first to account for any transparent pixels
            drawDrawable(fg);
            double fgLuminance = computer.computeLuminance(mFlatBitmap);

            // Start drawing from scratch and calculate background luminance
            mFlatCanvas.drawColor(Color.BLACK);
            drawDrawable(bg);
            double bgLuminance = computer.computeLuminance(mFlatBitmap);

            drawDrawable(fg);
            mLuminanceDiff = fgLuminance - bgLuminance;
        } else {
            // We do not have separate layer information.
            // Try to calculate everything from a single layer
            drawDrawable(bg);
            drawDrawable(fg);

            LuminanceComputer computer = createDefaultLuminanceComputer(ComputationType.SPREAD);
            mLuminanceDiff = computer.computeLuminance(mFlatBitmap, /* scale= */ true);
        }
        generateMono();
        return new InsetDrawable(this, -AdaptiveIconDrawable.getExtraInsetFraction());
    }

    public double getLuminanceDiff() {
        return mLuminanceDiff;
    }

    @WorkerThread
    private void generateMono() {
        mAlphaCanvas.drawBitmap(mFlatBitmap, 0, 0, mCopyPaint);

        // Scale the end points:
        ByteBuffer buffer = ByteBuffer.wrap(mPixels);
        buffer.rewind();
        mAlphaBitmap.copyPixelsToBuffer(buffer);
@@ -136,22 +162,10 @@ public class MonochromeIconFactory extends Drawable {
            // 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);
                mPixels[i] = (byte) (p2);
            }

            // Second phase of processing, aimed on increasing the contrast
+65 −59
Original line number Diff line number Diff line
@@ -24,9 +24,9 @@ import android.graphics.Bitmap.Config.HARDWARE
import android.graphics.BlendMode.SRC_IN
import android.graphics.BlendModeColorFilter
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
@@ -55,7 +55,6 @@ class MonoIconThemeController(
        factory: BaseIconFactory,
        sourceHint: SourceHint?,
    ): ThemedBitmap {

        val currentDelegateFactory = info.delegateFactory
        if (currentDelegateFactory is ClockAnimationInfo) {
            val fullDrawable = currentDelegateFactory.baseDrawableState.newDrawable()
@@ -73,41 +72,35 @@ class MonoIconThemeController(
            }
        }

        val mono =
            getMonochromeDrawable(
                icon,
                info,
                sourceHint?.isFileDrawable ?: false,
                shouldForceThemeIcon,
            )
        val mono = icon.monochrome
        if (mono != null) {
            return MonoThemedBitmap(
                factory.createIconBitmap(mono, 1f /* scale */, MODE_ALPHA, true /* isFullBleed */),
                factory.createIconBitmap(
                    InsetDrawable(mono, -getExtraInsetFraction()),
                    1f /* scale */,
                    MODE_ALPHA,
                    true, /* isFullBleed */
                ),
                colorProvider,
            )
        }
        return ThemedBitmap.NOT_SUPPORTED
    }

    /**
     * Returns a monochromatic version of the given drawable or null, if it is not supported
     *
     * @param base the original icon
     */
    private fun getMonochromeDrawable(
        base: AdaptiveIconDrawable,
        info: BitmapInfo,
        isFileDrawable: Boolean,
        shouldForceThemeIcon: Boolean,
    ): Drawable? {
        val mono = base.monochrome
        if (mono != null) {
            return InsetDrawable(mono, -AdaptiveIconDrawable.getExtraInsetFraction())
        }
        if (Flags.forceMonochromeAppIcons() && shouldForceThemeIcon && !isFileDrawable) {
            return MonochromeIconFactory(info.icon.width).wrap(base)
        if (Flags.forceMonochromeAppIcons() && shouldForceThemeIcon) {
            val monoFactory = MonochromeIconFactory(info.icon.width)
            val wrappedIcon = monoFactory.wrap(icon)
            return MonoThemedBitmap(
                factory.createIconBitmap(
                    wrappedIcon,
                    1f /* scale */,
                    MODE_ALPHA,
                    true, /* isFullBleed */
                ),
                colorProvider,
                monoFactory.luminanceDiff,
            )
        }
        return null

        return ThemedBitmap.NOT_SUPPORTED
    }

    override fun decode(
@@ -117,17 +110,31 @@ class MonoIconThemeController(
        sourceHint: SourceHint,
    ): ThemedBitmap {
        val icon = info.icon
        if (bytes.size != icon.height * icon.width) return ThemedBitmap.NOT_SUPPORTED
        val expectedSize = icon.height * icon.width

        var monoBitmap = Bitmap.createBitmap(icon.width, icon.height, ALPHA_8)
        monoBitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
        return when (bytes.size) {
            expectedSize -> {
                MonoThemedBitmap(
                    ByteBuffer.wrap(bytes).readMonoBitmap(icon.width, icon.height),
                    colorProvider,
                )
            }
            (expectedSize + MonoThemedBitmap.DOUBLE_BYTE_SIZE) -> {
                val buffer = ByteBuffer.wrap(bytes)
                val monoBitmap = buffer.readMonoBitmap(icon.width, icon.height)
                val luminanceDelta = buffer.asDoubleBuffer().get()
                MonoThemedBitmap(monoBitmap, colorProvider, luminanceDelta)
            }
            else -> ThemedBitmap.NOT_SUPPORTED
        }
    }

    private fun ByteBuffer.readMonoBitmap(width: Int, height: Int): Bitmap {
        val monoBitmap = Bitmap.createBitmap(width, height, ALPHA_8)
        monoBitmap.copyPixelsFromBuffer(this)

        val hwMonoBitmap = monoBitmap.copy(HARDWARE, false /*isMutable*/)
        if (hwMonoBitmap != null) {
            monoBitmap.recycle()
            monoBitmap = hwMonoBitmap
        }
        return MonoThemedBitmap(monoBitmap, colorProvider)
        return hwMonoBitmap?.also { monoBitmap.recycle() } ?: monoBitmap
    }

    override fun createThemedAdaptiveIcon(
@@ -135,29 +142,28 @@ class MonoIconThemeController(
        originalIcon: AdaptiveIconDrawable,
        info: BitmapInfo?,
    ): AdaptiveIconDrawable {
        val colors = colorProvider(context)

        originalIcon.mutate()
        var monoDrawable = originalIcon.monochrome?.apply { setTint(colors[1]) }
        originalIcon.monochrome?.let {
            val colors = colorProvider(context)
            it.setTint(colors[1])
            return@createThemedAdaptiveIcon AdaptiveIconDrawable(ColorDrawable(colors[0]), it)
        }

        val themedBitmap = info?.themedBitmap as? MonoThemedBitmap ?: return originalIcon
        val colors = themedBitmap.getUpdatedColors(context)

        if (monoDrawable == null) {
            info?.themedBitmap?.let { themedBitmap ->
                if (themedBitmap is MonoThemedBitmap) {
        // Inject a previously generated monochrome icon
        // Use BitmapDrawable instead of FastBitmapDrawable so that the colorState is
        // preserved in constantState
        // Inset the drawable according to the AdaptiveIconDrawable layers
                    monoDrawable =
        val monoDrawable =
            InsetDrawable(
                BitmapDrawable(themedBitmap.mono).apply {
                    colorFilter = BlendModeColorFilter(colors[1], SRC_IN)
                },
                            AdaptiveIconDrawable.getExtraInsetFraction() / 2,
                getExtraInsetFraction() / 2,
            )
                }
            }
        }

        return monoDrawable?.let { AdaptiveIconDrawable(ColorDrawable(colors[0]), it) }
            ?: originalIcon
        return AdaptiveIconDrawable(ColorDrawable(colors[0]), monoDrawable)
    }
}
Loading