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

Commit 0f21878a authored by Charlie Anderson's avatar Charlie Anderson
Browse files

Add customization options for flagging and shaping full-bleed icons

- Does not shape themed icons yet

Bug: 421883017
Test: screenshot tests
Flag: com.android.launcher3.enable_launcher_icon_shapes

Change-Id: I86b43f2352cce828f98b7402867b39daba08a8b5
parent e85351bd
Loading
Loading
Loading
Loading
+88 −22
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ import static android.graphics.Paint.DITHER_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;

import static com.android.launcher3.icons.BitmapInfo.FLAG_FULL_BLEED;
import static com.android.launcher3.icons.BitmapInfo.FLAG_INSTANT;
import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
@@ -86,6 +87,7 @@ public class BaseIconFactory implements AutoCloseable {
    @NonNull
    private final PackageManager mPm;

    public final boolean mDrawFullBleedIcons;
    protected final int mFullResIconDpi;
    protected final int mIconBitmapSize;

@@ -103,15 +105,16 @@ public class BaseIconFactory implements AutoCloseable {

    private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(245, 245, 245);

    protected BaseIconFactory(Context context, int fullResIconDpi, int iconBitmapSize,
            boolean unused) {
        this(context, fullResIconDpi, iconBitmapSize);
    public BaseIconFactory(Context context, int fullResIconDpi, int iconBitmapSize) {
        this(context, fullResIconDpi, iconBitmapSize, /* drawFullBleedIcons */ false);
    }

    public BaseIconFactory(Context context, int fullResIconDpi, int iconBitmapSize) {
    public BaseIconFactory(Context context, int fullResIconDpi, int iconBitmapSize,
            boolean drawFullBleedIcons) {
        mContext = context.getApplicationContext();
        mFullResIconDpi = fullResIconDpi;
        mIconBitmapSize = iconBitmapSize;
        mDrawFullBleedIcons = drawFullBleedIcons;

        mPm = mContext.getPackageManager();

@@ -171,7 +174,7 @@ public class BaseIconFactory implements AutoCloseable {
                new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR),
                new CenterTextDrawable(placeholder, color));
        Bitmap icon = createIconBitmap(drawable, ICON_VISIBLE_AREA_FACTOR);
        return BitmapInfo.of(icon, color);
        return BitmapInfo.of(icon, color, getDefaultIconShape());
    }

    public BitmapInfo createIconBitmap(Bitmap icon) {
@@ -179,7 +182,8 @@ public class BaseIconFactory implements AutoCloseable {
            icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
        }

        return BitmapInfo.of(icon, ColorExtractor.findDominantColorByHue(icon));
        return BitmapInfo.of(icon, ColorExtractor.findDominantColorByHue(icon),
                getDefaultIconShape());
    }

    /**
@@ -223,10 +227,12 @@ public class BaseIconFactory implements AutoCloseable {
        }
        AdaptiveIconDrawable adaptiveIcon = normalizeAndWrapToAdaptiveIcon(tempIcon, scale);
        Bitmap bitmap = createIconBitmap(adaptiveIcon, scale[0],
                options == null ? MODE_WITH_SHADOW : options.mGenerationMode);
                options == null ? MODE_WITH_SHADOW : options.mGenerationMode, mDrawFullBleedIcons);
        int color = (options != null && options.mExtractedColor != null)
                ? options.mExtractedColor : ColorExtractor.findDominantColorByHue(bitmap);
        BitmapInfo info = BitmapInfo.of(bitmap, color);

        BitmapInfo info = BitmapInfo.of(bitmap, color, getDefaultIconShape());
        if (mDrawFullBleedIcons) info.flags |= FLAG_FULL_BLEED;

        if (adaptiveIcon instanceof Extender extender) {
            info = extender.getExtendedInfo(bitmap, color, this, scale[0]);
@@ -248,6 +254,18 @@ public class BaseIconFactory implements AutoCloseable {
        return info;
    }

    /**
     * Generates an IconShape based on the current bitmap size and default icon mask.
     */
    public IconShape getDefaultIconShape() {
        if (!mDrawFullBleedIcons) return IconShape.EMPTY;
        AdaptiveIconDrawable tempAdaptiveIcon =
                new AdaptiveIconDrawable(new ColorDrawable(BLACK), null);
        tempAdaptiveIcon.setBounds(0, 0, mIconBitmapSize, mIconBitmapSize);
        return new IconShape(mIconBitmapSize, tempAdaptiveIcon.getIconMask(),
                getWhiteShadowLayer());
    }

    @NonNull
    public FlagOp getBitmapFlagOp(@Nullable IconOptions options) {
        FlagOp op = FlagOp.NO_OP;
@@ -340,7 +358,7 @@ public class BaseIconFactory implements AutoCloseable {
    public Bitmap createScaledBitmap(@NonNull Drawable icon, @BitmapGenerationMode int mode) {
        float[] scale = new float[1];
        icon = normalizeAndWrapToAdaptiveIcon(icon, scale);
        return createIconBitmap(icon, Math.min(scale[0], ICON_SCALE_FOR_SHADOWS), mode);
        return createIconBitmap(icon, Math.min(scale[0], ICON_SCALE_FOR_SHADOWS), mode, false);
    }

    /**
@@ -397,12 +415,12 @@ public class BaseIconFactory implements AutoCloseable {

    @NonNull
    public Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale) {
        return createIconBitmap(icon, scale, MODE_DEFAULT);
        return createIconBitmap(icon, scale, MODE_DEFAULT, mDrawFullBleedIcons);
    }

    @NonNull
    public Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale,
            @BitmapGenerationMode int bitmapGenerationMode) {
            @BitmapGenerationMode int bitmapGenerationMode, boolean isFullBleed) {
        final int size = mIconBitmapSize;
        final Bitmap bitmap;
        switch (bitmapGenerationMode) {
@@ -412,7 +430,8 @@ public class BaseIconFactory implements AutoCloseable {
            case MODE_HARDWARE:
            case MODE_HARDWARE_WITH_SHADOW: {
                return BitmapRenderer.createHardwareBitmap(size, size, canvas ->
                        drawIconBitmap(canvas, icon, scale, bitmapGenerationMode, null));
                        drawIconBitmap(canvas, icon, scale, bitmapGenerationMode, null,
                                isFullBleed));
            }
            case MODE_WITH_SHADOW:
            default:
@@ -423,36 +442,42 @@ public class BaseIconFactory implements AutoCloseable {
            return bitmap;
        }
        mCanvas.setBitmap(bitmap);
        drawIconBitmap(mCanvas, icon, scale, bitmapGenerationMode, bitmap);
        drawIconBitmap(mCanvas, icon, scale, bitmapGenerationMode, bitmap, isFullBleed);
        mCanvas.setBitmap(null);
        return bitmap;
    }

    private void drawIconBitmap(@NonNull Canvas canvas, @Nullable Drawable icon,
            final float scale, @BitmapGenerationMode int bitmapGenerationMode,
            @Nullable Bitmap targetBitmap) {
            @Nullable Bitmap targetBitmap, boolean isFullBleed) {
        final int size = mIconBitmapSize;
        mOldBounds.set(icon.getBounds());
        boolean isFullBleedEnabled = isFullBleed
                && Flags.enableLauncherIconShapes();
        if (icon instanceof AdaptiveIconDrawable aid) {
            // We are ignoring KEY_SHADOW_DISTANCE because regular icons ignore this at the
            // moment b/298203449
            int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
            int offset = isFullBleedEnabled
                    ? 0
                    : Math.max((int) Math.ceil(BLUR_FACTOR * size),
                            Math.round(size * (1 - scale) / 2));
            // b/211896569: AdaptiveIconDrawable do not work properly for non top-left bounds
            int newBounds = size - offset * 2;
            icon.setBounds(0, 0, newBounds, newBounds);
            Path shapePath = getShapePath(aid, icon.getBounds());
            int count = canvas.save();
            canvas.translate(offset, offset);
            if (bitmapGenerationMode == MODE_WITH_SHADOW
                    || bitmapGenerationMode == MODE_HARDWARE_WITH_SHADOW) {
                getShadowGenerator().addPathShadow(shapePath, canvas);

            if ((bitmapGenerationMode == MODE_WITH_SHADOW
                    || bitmapGenerationMode == MODE_HARDWARE_WITH_SHADOW)
                    && !isFullBleedEnabled) {
                getShadowGenerator().addPathShadow(aid.getIconMask(), canvas);
            }

            if (icon instanceof Extender) {
                ((Extender) icon).drawForPersistence(canvas);
            } else {
                drawAdaptiveIcon(canvas, aid, shapePath);
                drawAdaptiveIcon(canvas, aid, isFullBleedEnabled,
                        getShapePath(aid, icon.getBounds()));
            }

            canvas.restoreToCount(count);
@@ -508,8 +533,11 @@ public class BaseIconFactory implements AutoCloseable {
     * @param canvas    canvas to draw on
     * @param drawable  AdaptiveIconDrawable to draw
     * @param shapePath path to clip icon with for shapes
     *
     * @deprecated b/421884219 use mDrawFullBleedIcons and shape it using {@link IconShape} instead
     */
    protected void drawAdaptiveIcon(
    @Deprecated
    private void drawShapedAdaptiveIcon(
            @NonNull Canvas canvas,
            @NonNull AdaptiveIconDrawable drawable,
            @NonNull Path shapePath
@@ -526,6 +554,44 @@ public class BaseIconFactory implements AutoCloseable {
        canvas.drawPath(shapePath, paint);
    }

    /**
     * Draws AdaptiveIconDrawable onto canvas with either default shape, or
     * as Full-bleed.
     *
     * @param canvas    canvas to draw on
     * @param drawable  AdaptiveIconDrawable to draw
     * @param isFullBleed whether to draw as full-bleed.
     */
    private void drawAdaptiveIcon(
            @NonNull Canvas canvas,
            @NonNull AdaptiveIconDrawable drawable,
            boolean isFullBleed,
            Path shape
    ) {
        Drawable background = drawable.getBackground();
        Drawable foreground = drawable.getForeground();
        boolean shouldNotDrawFullBleed = !isFullBleed || (background == null && foreground == null);
        if (shouldNotDrawFullBleed) {
            boolean shouldDrawDefaultShape = !isFullBleed && mDrawFullBleedIcons;
            // TODO: b/421884219 Temporarily keep old icon shape implementation until migrated
            if (shouldDrawDefaultShape) {
                // New Icon shapes path, used for non-full bleed icons
                drawable.draw(canvas);
            } else {
                // Old Icon shapes path, to get old shape effect if mDrawFullBleedIcons is false
                drawShapedAdaptiveIcon(canvas, drawable, shape);
            }
            return;
        }
        canvas.drawColor(Color.BLACK);
        if (background != null) {
            background.draw(canvas);
        }
        if (foreground != null) {
            foreground.draw(canvas);
        }
    }

    @Override
    public void close() {
        clear();
+34 −18
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.launcher3.icons
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Path
import android.graphics.drawable.Drawable
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
@@ -30,12 +29,21 @@ import com.android.launcher3.util.FlagOp
open class BitmapInfo(
    @JvmField val icon: Bitmap,
    @JvmField val color: Int,
    @JvmField val defaultIconShape: IconShape = IconShape.EMPTY,
    @BitmapInfoFlags @JvmField var flags: Int = 0,
    var themedBitmap: ThemedBitmap? = null,
) {
    @IntDef(
        flag = true,
        value = [FLAG_WORK, FLAG_INSTANT, FLAG_CLONE, FLAG_PRIVATE, FLAG_WRAPPED_NON_ADAPTIVE],
        value =
            [
                FLAG_WORK,
                FLAG_INSTANT,
                FLAG_CLONE,
                FLAG_PRIVATE,
                FLAG_WRAPPED_NON_ADAPTIVE,
                FLAG_FULL_BLEED,
            ],
    )
    internal annotation class BitmapInfoFlags

@@ -59,7 +67,7 @@ open class BitmapInfo(

    @Override
    open fun clone(): BitmapInfo {
        return copyInternalsTo(BitmapInfo(icon, color))
        return copyInternalsTo(BitmapInfo(icon, color, defaultIconShape))
    }

    protected fun copyInternalsTo(target: BitmapInfo): BitmapInfo {
@@ -101,17 +109,21 @@ open class BitmapInfo(
     *
     * @param context Context
     * @param creationFlags Flags for creating the FastBitmapDrawable
     * @param badgeShape Optional Path for masking icon badges to a shape. Should be 100x100.
     * @param iconShape information for custom Icon Shapes, to use with Full-bleed icons.
     * @return FastBitmapDrawable
     */
    open fun newIcon(
        context: Context,
        @DrawableCreationFlags creationFlags: Int,
        badgeShape: Path?,
        iconShape: IconShape?,
    ): FastBitmapDrawable {
        val drawable: FastBitmapDrawable =
            if (isLowRes) {
                FastBitmapDrawable(this, PlaceHolderDelegateFactory(context))
                FastBitmapDrawable(
                    this,
                    iconShape ?: defaultIconShape,
                    PlaceHolderDelegateFactory(context),
                )
            } else if (
                (creationFlags and FLAG_THEMED) != 0 &&
                    themedBitmap != null &&
@@ -119,9 +131,9 @@ open class BitmapInfo(
            ) {
                themedBitmap!!.newDrawable(this, context)
            } else {
                FastBitmapDrawable(this)
                FastBitmapDrawable(this, iconShape ?: defaultIconShape)
            }
        applyFlags(context, drawable, creationFlags, badgeShape)
        applyFlags(context, drawable, creationFlags)
        return drawable
    }

@@ -129,7 +141,6 @@ open class BitmapInfo(
        context: Context,
        drawable: FastBitmapDrawable,
        @DrawableCreationFlags creationFlags: Int,
        badgeShape: Path?,
    ) {
        this.creationFlags = creationFlags
        drawable.disabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f)
@@ -140,7 +151,6 @@ open class BitmapInfo(
                    context,
                    (creationFlags and FLAG_THEMED) != 0,
                    (creationFlags and FLAG_SKIP_USER_BADGE) != 0,
                    badgeShape,
                )
            if (badge != null) {
                drawable.badge = badge
@@ -156,8 +166,8 @@ open class BitmapInfo(
     * @param badgeShape Optional Path to mask badges to a shape. Should be 100x100.
     * @return Drawable for the badge.
     */
    fun getBadgeDrawable(context: Context, isThemed: Boolean, badgeShape: Path?): Drawable? {
        return getBadgeDrawable(context, isThemed, false, badgeShape)
    fun getBadgeDrawable(context: Context, isThemed: Boolean): Drawable? {
        return getBadgeDrawable(context, isThemed, false)
    }

    /**
@@ -173,20 +183,19 @@ open class BitmapInfo(
        context: Context,
        isThemed: Boolean,
        skipUserBadge: Boolean,
        badgeShape: Path?,
    ): Drawable? {
        if (badgeInfo != null) {
            var creationFlag = if (isThemed) FLAG_THEMED else 0
            if (skipUserBadge) {
                creationFlag = creationFlag or FLAG_SKIP_USER_BADGE
            }
            return badgeInfo!!.newIcon(context, creationFlag, badgeShape)
            return badgeInfo!!.newIcon(context, creationFlag, null)
        }
        if (skipUserBadge) {
            return null
        } else {
            getBadgeDrawableInfo()?.let {
                return UserBadgeDrawable(context, it.drawableRes, it.colorRes, isThemed, badgeShape)
                return UserBadgeDrawable(context, it.drawableRes, it.colorRes, isThemed)
            }
        }
        return null
@@ -245,6 +254,7 @@ open class BitmapInfo(
        const val FLAG_CLONE: Int = 1 shl 2
        const val FLAG_PRIVATE: Int = 1 shl 3
        const val FLAG_WRAPPED_NON_ADAPTIVE: Int = 1 shl 4
        const val FLAG_FULL_BLEED: Int = 1 shl 5

        // Drawable creation flags
        const val FLAG_THEMED: Int = 1 shl 0
@@ -256,12 +266,18 @@ open class BitmapInfo(

        @JvmStatic
        fun fromBitmap(bitmap: Bitmap): BitmapInfo {
            return of(bitmap, 0)
            return of(bitmap, 0, IconShape.EMPTY)
        }

        @JvmStatic
        fun of(bitmap: Bitmap, color: Int): BitmapInfo {
            return BitmapInfo(bitmap, color)
        fun of(bitmap: Bitmap, color: Int, defaultShape: IconShape = IconShape.EMPTY): BitmapInfo {
            val flags = if (defaultShape == IconShape.EMPTY) 0 else FLAG_FULL_BLEED
            return BitmapInfo(
                icon = bitmap,
                color = color,
                defaultIconShape = defaultShape,
                flags = flags,
            )
        }
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -78,7 +78,7 @@ public class BubbleIconFactory extends BaseIconFactory {
            outScale = new float[1];
        }
        icon = normalizeAndWrapToAdaptiveIcon(icon, outScale);
        return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW);
        return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW, mDrawFullBleedIcons);
    }

    /**
@@ -95,7 +95,7 @@ public class BubbleIconFactory extends BaseIconFactory {
            userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
        }
        Bitmap userBadgedBitmap = mBadgeFactory.createIconBitmap(
                userBadgedAppIcon, 1, MODE_WITH_SHADOW);
                userBadgedAppIcon, 1, MODE_WITH_SHADOW, mDrawFullBleedIcons);
        return mBadgeFactory.createIconBitmap(userBadgedBitmap);
    }

+13 −8
Original line number Diff line number Diff line
@@ -25,7 +25,6 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable
@@ -170,7 +169,7 @@ class ClockDrawableWrapper private constructor(base: AdaptiveIconDrawable) :
        val mFlattenedBackground: Bitmap,
        val themeData: AnimationInfo?,
        val themeBackground: Bitmap?,
    ) : BitmapInfo(icon, color, /* flags */ 0, /* themedBitmap */ null) {
    ) : BitmapInfo(icon, color, flags = 0, themedBitmap = null) {
        val boundsOffset: Float =
            max(ShadowGenerator.BLUR_FACTOR.toDouble(), ((1 - scale) / 2).toDouble()).toFloat()

@@ -178,7 +177,7 @@ class ClockDrawableWrapper private constructor(base: AdaptiveIconDrawable) :
        override fun newIcon(
            context: Context,
            @DrawableCreationFlags creationFlags: Int,
            badgeShape: Path?,
            iconShape: IconShape?,
        ): FastBitmapDrawable {
            val bg: Bitmap
            val themedFgColor: Int
@@ -197,11 +196,10 @@ class ClockDrawableWrapper private constructor(base: AdaptiveIconDrawable) :
                bg = mFlattenedBackground
                bgFilter = null
            }

            val delegateInfo =
                ClockDelegateInfo(themedFgColor, boundsOffset, animInfo, bg, bgFilter)
            val d = FastBitmapDrawable(this, delegateInfo)
            applyFlags(context, d, creationFlags, null)
            val d = FastBitmapDrawable(this, iconShape ?: defaultIconShape, delegateInfo)
            applyFlags(context, d, creationFlags)
            return d
        }

@@ -235,6 +233,7 @@ class ClockDrawableWrapper private constructor(base: AdaptiveIconDrawable) :
    ) : DelegateFactory {
        override fun newDelegate(
            bitmapInfo: BitmapInfo,
            iconShape: IconShape,
            paint: Paint,
            host: FastBitmapDrawable,
        ): FastBitmapDrawableDelegate {
@@ -289,9 +288,15 @@ class ClockDrawableWrapper private constructor(base: AdaptiveIconDrawable) :
            mFullDrawable.setBounds(0, 0, bounds.width(), bounds.height())
        }

        override fun drawContent(info: BitmapInfo, canvas: Canvas, bounds: Rect, paint: Paint) {
        override fun drawContent(
            info: BitmapInfo,
            host: FastBitmapDrawable,
            canvas: Canvas,
            bounds: Rect,
            paint: Paint,
        ) {
            if (mAnimInfo == null) {
                super.drawContent(info, canvas, bounds, paint)
                super.drawContent(info, mHost, canvas, bounds, paint)
                return
            }
            canvas.drawBitmap(mBG, null, bounds, mBgPaint)
+43 −4
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.launcher3.icons
import android.R
import android.animation.ObjectAnimator
import android.graphics.Bitmap
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
@@ -28,6 +29,7 @@ import android.graphics.Paint.ANTI_ALIAS_FLAG
import android.graphics.Paint.FILTER_BITMAP_FLAG
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.Shader.TileMode.CLAMP
import android.graphics.drawable.Drawable
import android.graphics.drawable.Drawable.Callback
import android.util.FloatProperty
@@ -36,26 +38,32 @@ import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import android.view.animation.PathInterpolator
import androidx.annotation.VisibleForTesting
import com.android.launcher3.icons.BitmapInfo.Companion.FLAG_FULL_BLEED
import com.android.launcher3.icons.BitmapInfo.Companion.LOW_RES_INFO
import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags
import com.android.launcher3.icons.FastBitmapDrawableDelegate.DelegateFactory
import com.android.launcher3.icons.FastBitmapDrawableDelegate.SimpleDelegateFactory
import com.android.launcher3.icons.ShadowGenerator.ICON_SCALE_FOR_SHADOWS
import kotlin.math.min

class FastBitmapDrawable
@JvmOverloads
constructor(
    info: BitmapInfo?,
    private val iconShape: IconShape = IconShape.EMPTY,
    private val delegateFactory: DelegateFactory = SimpleDelegateFactory,
) : Drawable(), Callback {

    @JvmOverloads constructor(b: Bitmap, iconColor: Int = 0) : this(BitmapInfo.of(b, iconColor))

    @JvmField val bitmapInfo: BitmapInfo = info ?: BitmapInfo.LOW_RES_INFO
    // b/404578798 - mBitmapInfo isn't expected to be null, but it is in some cases.
    @JvmField val bitmapInfo: BitmapInfo = info ?: LOW_RES_INFO
    var isAnimationEnabled: Boolean = true

    @JvmField protected val paint: Paint = Paint(FILTER_BITMAP_FLAG or ANTI_ALIAS_FLAG)
    private val shader: BitmapShader = BitmapShader(bitmapInfo.icon, CLAMP, CLAMP)

    val delegate = delegateFactory.newDelegate(bitmapInfo, paint, this)
    val delegate = delegateFactory.newDelegate(bitmapInfo, iconShape, paint, this)

    @JvmField @VisibleForTesting var isPressed: Boolean = false
    @JvmField @VisibleForTesting var isHovered: Boolean = false
@@ -117,10 +125,39 @@ constructor(
    }

    private fun drawInternal(canvas: Canvas, bounds: Rect) {
        delegate.drawContent(bitmapInfo, canvas, bounds, paint)
        delegate.drawContent(bitmapInfo, this, canvas, bounds, paint)
        badge?.draw(canvas)
    }

    fun drawContent(canvas: Canvas, bounds: Rect) {
        if ((bitmapInfo.flags and FLAG_FULL_BLEED) != 0) {
            drawShapedInternal(canvas, bounds)
        } else {
            canvas.drawBitmap(bitmapInfo.icon, null, bounds, paint)
        }
    }

    private fun drawShapedInternal(canvas: Canvas, bounds: Rect) {
        canvas.save()
        canvas.drawBitmap(iconShape.shadowLayer, null, bounds, paint)

        canvas.translate(bounds.left.toFloat(), bounds.top.toFloat())
        val iconWidth: Int = bitmapInfo.icon.width
        val iconHeight: Int = bitmapInfo.icon.height

        canvas.scale(bounds.width().toFloat() / iconWidth, bounds.height().toFloat() / iconHeight)
        canvas.scale(
            ICON_SCALE_FOR_SHADOWS,
            ICON_SCALE_FOR_SHADOWS,
            (iconWidth / 2).toFloat(),
            (iconHeight / 2).toFloat(),
        )
        paint.shader = shader
        canvas.drawPath(iconShape.path, paint)
        paint.shader = null
        canvas.restore()
    }

    /** Returns the primary icon color, slightly tinted white */
    fun getIconColor(): Int = delegate.getIconColor(bitmapInfo)

@@ -249,6 +286,7 @@ constructor(
            bitmapInfo,
            isDisabled,
            badge?.constantState,
            iconShape,
            creationFlags,
            delegateFactory,
            level,
@@ -277,13 +315,14 @@ constructor(
        val bitmapInfo: BitmapInfo,
        val isDisabled: Boolean,
        val badgeConstantState: ConstantState?,
        val iconShape: IconShape,
        val creationFlags: Int,
        val delegateFactory: DelegateFactory,
        val level: Int,
    ) : ConstantState() {

        override fun newDrawable(): FastBitmapDrawable {
            val drawable = FastBitmapDrawable(bitmapInfo, delegateFactory)
            val drawable = FastBitmapDrawable(bitmapInfo, iconShape, delegateFactory)
            drawable.isDisabled = isDisabled
            if (badgeConstantState != null) {
                drawable.badge = badgeConstantState.newDrawable()
Loading