/* * Copyright (C) 2019 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.intentresolver; import static android.content.Context.ACTIVITY_SERVICE; import static android.graphics.Paint.DITHER_FLAG; import static android.graphics.Paint.FILTER_BITMAP_FLAG; import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction; import android.annotation.AttrRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.graphics.Bitmap; import android.graphics.BlurMaskFilter; import android.graphics.BlurMaskFilter.Blur; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; 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.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.DrawableWrapper; import android.os.UserHandle; import android.util.AttributeSet; import android.util.Pools.SynchronizedPool; import android.util.TypedValue; import com.android.internal.annotations.VisibleForTesting; import org.xmlpull.v1.XmlPullParser; import java.nio.ByteBuffer; import java.util.Optional; /** * @deprecated Use the Launcher3 Iconloaderlib at packages/apps/Launcher3/iconloaderlib. This class * is a temporary fork of Iconloader. It combines all necessary methods to render app icons that are * possibly badged. It is intended to be used only by Sharesheet for the Q release with custom code. */ @Deprecated public class SimpleIconFactory { private static final SynchronizedPool sPool = new SynchronizedPool<>(Runtime.getRuntime().availableProcessors()); private static boolean sPoolEnabled = true; private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; private static final float BLUR_FACTOR = 1.5f / 48; private Context mContext; private Canvas mCanvas; private PackageManager mPm; private int mFillResIconDpi; private int mIconBitmapSize; private int mBadgeBitmapSize; private int mWrapperBackgroundColor; private Drawable mWrapperIcon; private final Rect mOldBounds = new Rect(); /** * Obtain a SimpleIconFactory from a pool objects. * * @deprecated Do not use, functionality will be replaced by iconloader lib eventually. */ @Deprecated public static SimpleIconFactory obtain(Context ctx) { SimpleIconFactory instance = sPoolEnabled ? sPool.acquire() : null; if (instance == null) { final ActivityManager am = (ActivityManager) ctx.getSystemService(ACTIVITY_SERVICE); final int iconDpi = (am == null) ? 0 : am.getLauncherLargeIconDensity(); final int iconSize = getIconSizeFromContext(ctx); final int badgeSize = getBadgeSizeFromContext(ctx); instance = new SimpleIconFactory(ctx, iconDpi, iconSize, badgeSize); instance.setWrapperBackgroundColor(Color.WHITE); } return instance; } /** * Enables or disables SimpleIconFactory objects pooling. It is enabled in production, you * could use this method in tests and disable the pooling to make the icon rendering more * deterministic because some sizing parameters will not be cached. Please ensure that you * reset this value back after finishing the test. */ @VisibleForTesting public static void setPoolEnabled(boolean poolEnabled) { sPoolEnabled = poolEnabled; } private static int getAttrDimFromContext(Context ctx, @AttrRes int attrId, String errorMsg) { final Resources res = ctx.getResources(); TypedValue outVal = new TypedValue(); if (!ctx.getTheme().resolveAttribute(attrId, outVal, true)) { throw new IllegalStateException(errorMsg); } return res.getDimensionPixelSize(outVal.resourceId); } private static int getIconSizeFromContext(Context ctx) { return getAttrDimFromContext(ctx, com.android.internal.R.attr.iconfactoryIconSize, "Expected theme to define iconfactoryIconSize."); } private static int getBadgeSizeFromContext(Context ctx) { return getAttrDimFromContext(ctx, com.android.internal.R.attr.iconfactoryBadgeSize, "Expected theme to define iconfactoryBadgeSize."); } /** * Recycles the SimpleIconFactory so others may use it. * * @deprecated Do not use, functionality will be replaced by iconloader lib eventually. */ @Deprecated public void recycle() { // Return to default background color setWrapperBackgroundColor(Color.WHITE); sPool.release(this); } /** * @deprecated Do not use, functionality will be replaced by iconloader lib eventually. */ @Deprecated private SimpleIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, int badgeBitmapSize) { mContext = context.getApplicationContext(); mPm = mContext.getPackageManager(); mIconBitmapSize = iconBitmapSize; mBadgeBitmapSize = badgeBitmapSize; mFillResIconDpi = fillResIconDpi; mCanvas = new Canvas(); mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); // Normalizer init // Use twice the icon size as maximum size to avoid scaling down twice. mMaxSize = iconBitmapSize * 2; mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8); mScaleCheckCanvas = new Canvas(mBitmap); mPixels = new byte[mMaxSize * mMaxSize]; mLeftBorder = new float[mMaxSize]; mRightBorder = new float[mMaxSize]; mBounds = new Rect(); mAdaptiveIconBounds = new Rect(); mAdaptiveIconScale = SCALE_NOT_INITIALIZED; // Shadow generator init mDefaultBlurMaskFilter = new BlurMaskFilter(iconBitmapSize * BLUR_FACTOR, Blur.NORMAL); } /** * Sets the background color used for wrapped adaptive icon * * @deprecated Do not use, functionality will be replaced by iconloader lib eventually. */ @Deprecated void setWrapperBackgroundColor(int color) { mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color; } /** * Creates bitmap using the source drawable and various parameters. * The bitmap is visually normalized with other icons and has enough spacing to add shadow. * Note: this method has been modified from iconloaderlib to remove a profile diff check. * * @param icon source of the icon associated with a user that has no badge, * likely user 0 * @param user info can be used for a badge * @return a bitmap suitable for disaplaying as an icon at various system UIs. * * @deprecated Do not use, functionality will be replaced by iconloader lib eventually. */ @Deprecated Bitmap createUserBadgedIconBitmap(@Nullable Drawable icon, @Nullable UserHandle user) { float [] scale = new float[1]; // If no icon is provided use the system default if (icon == null) { icon = getFullResDefaultActivityIcon(mFillResIconDpi); } icon = normalizeAndWrapToAdaptiveIcon(icon, null, scale); Bitmap bitmap = createIconBitmap(icon, scale[0]); if (icon instanceof AdaptiveIconDrawable) { mCanvas.setBitmap(bitmap); recreateIcon(Bitmap.createBitmap(bitmap), mCanvas); mCanvas.setBitmap(null); } final Bitmap result; if (user != null /* if modification from iconloaderlib */) { BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap); Drawable badged = mPm.getUserBadgedIcon(drawable, user); if (badged instanceof BitmapDrawable) { result = ((BitmapDrawable) badged).getBitmap(); } else { result = createIconBitmap(badged, 1f); } } else { result = bitmap; } return result; } /** * Creates bitmap using the source drawable and flattened pre-rendered app icon. * The bitmap is visually normalized with other icons and has enough spacing to add shadow. * This is custom functionality added to Iconloaderlib that will need to be ported. * * @param icon source of the icon associated with a user that has no badge * @param renderedAppIcon pre-rendered app icon to use as a badge, likely the output * of createUserBadgedIconBitmap for user 0 * @return a bitmap suitable for disaplaying as an icon at various system UIs. * * @deprecated Do not use, functionality will be replaced by iconloader lib eventually. */ @Deprecated public Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) { // If no icon is provided use the system default if (icon == null) { icon = getFullResDefaultActivityIcon(mFillResIconDpi); } // Direct share icons cannot be adaptive, most will arrive as bitmaps. To get reliable // presentation, force all DS icons to be circular. Scale DS image so it completely fills. int w = icon.getIntrinsicWidth(); int h = icon.getIntrinsicHeight(); float scale = 1; if (h > w && w > 0) { scale = (float) h / w; } else if (w > h && h > 0) { scale = (float) w / h; } Bitmap bitmap = createIconBitmapNoInsetOrMask(icon, scale); bitmap = maskBitmapToCircle(bitmap); icon = new BitmapDrawable(mContext.getResources(), bitmap); // We now have a circular masked and scaled icon, inset and apply shadow scale = getScale(icon, null); bitmap = createIconBitmap(icon, scale); mCanvas.setBitmap(bitmap); recreateIcon(Bitmap.createBitmap(bitmap), mCanvas); if (renderedAppIcon != null) { // Now scale down and apply the badge to the bottom right corner of the flattened icon renderedAppIcon = Bitmap.createScaledBitmap(renderedAppIcon, mBadgeBitmapSize, mBadgeBitmapSize, false); // Paint the provided badge on top of the flattened icon mCanvas.drawBitmap(renderedAppIcon, mIconBitmapSize - mBadgeBitmapSize, mIconBitmapSize - mBadgeBitmapSize, null); } mCanvas.setBitmap(null); return bitmap; } private Bitmap maskBitmapToCircle(Bitmap bitmap) { final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(output); final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG); // Apply an offset to enable shadow to be drawn final int size = bitmap.getWidth(); int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size), 1); // Draw mask paint.setColor(0xffffffff); canvas.drawARGB(0, 0, 0, 0); canvas.drawCircle(bitmap.getWidth() / 2f, bitmap.getHeight() / 2f, bitmap.getWidth() / 2f - offset, paint); // Draw masked bitmap paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); canvas.drawBitmap(bitmap, rect, rect, paint); return output; } private static Drawable getFullResDefaultActivityIcon(int iconDpi) { return Resources.getSystem().getDrawableForDensity(android.R.mipmap.sym_def_app_icon, iconDpi); } private Bitmap createIconBitmap(Drawable icon, float scale) { return createIconBitmap(icon, scale, mIconBitmapSize, true, false); } private Bitmap createIconBitmapNoInsetOrMask(Drawable icon, float scale) { return createIconBitmap(icon, scale, mIconBitmapSize, false, true); } /** * @param icon drawable that should be flattened to a bitmap * @param scale the scale to apply before drawing {@param icon} on the canvas * @param insetAdiForShadow when rendering AdaptiveIconDrawables inset to make room for a shadow * @param ignoreAdiMask when rendering AdaptiveIconDrawables ignore the current system mask */ private Bitmap createIconBitmap(Drawable icon, float scale, int size, boolean insetAdiForShadow, boolean ignoreAdiMask) { Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); mCanvas.setBitmap(bitmap); mOldBounds.set(icon.getBounds()); if (icon instanceof AdaptiveIconDrawable) { final AdaptiveIconDrawable adi = (AdaptiveIconDrawable) icon; // By default assumes the output bitmap will have a shadow directly applied and makes // room for it by insetting. If there are intermediate steps before applying the shadow // insetting is disableable. int offset = Math.round(size * (1 - scale) / 2); if (insetAdiForShadow) { offset = Math.max((int) Math.ceil(BLUR_FACTOR * size), offset); } Rect bounds = new Rect(offset, offset, size - offset, size - offset); // AdaptiveIconDrawables are by default masked by the user's icon shape selection. // If further masking is to be done, directly render to avoid the system masking. if (ignoreAdiMask) { final int cX = bounds.width() / 2; final int cY = bounds.height() / 2; final float portScale = 1f / (1 + 2 * getExtraInsetFraction()); final int insetWidth = (int) (bounds.width() / (portScale * 2)); final int insetHeight = (int) (bounds.height() / (portScale * 2)); Rect childRect = new Rect(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight); Optional.ofNullable(adi.getBackground()).ifPresent(drawable -> { drawable.setBounds(childRect); drawable.draw(mCanvas); }); Optional.ofNullable(adi.getForeground()).ifPresent(drawable -> { drawable.setBounds(childRect); drawable.draw(mCanvas); }); } else { adi.setBounds(bounds); adi.draw(mCanvas); } } else { if (icon instanceof BitmapDrawable) { BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; Bitmap b = bitmapDrawable.getBitmap(); if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) { bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics()); } } int width = size; int height = size; int intrinsicWidth = icon.getIntrinsicWidth(); int intrinsicHeight = icon.getIntrinsicHeight(); if (intrinsicWidth > 0 && intrinsicHeight > 0) { // Scale the icon proportionally to the icon dimensions final float ratio = (float) intrinsicWidth / intrinsicHeight; if (intrinsicWidth > intrinsicHeight) { height = (int) (width / ratio); } else if (intrinsicHeight > intrinsicWidth) { width = (int) (height * ratio); } } final int left = (size - width) / 2; final int top = (size - height) / 2; icon.setBounds(left, top, left + width, top + height); mCanvas.save(); mCanvas.scale(scale, scale, size / 2, size / 2); icon.draw(mCanvas); mCanvas.restore(); } icon.setBounds(mOldBounds); mCanvas.setBitmap(null); return bitmap; } private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, RectF outIconBounds, float[] outScale) { float scale = 1f; if (mWrapperIcon == null) { mWrapperIcon = mContext.getDrawable( R.drawable.iconfactory_adaptive_icon_drawable_wrapper).mutate(); } AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; dr.setBounds(0, 0, 1, 1); scale = getScale(icon, outIconBounds); if (!(icon instanceof AdaptiveIconDrawable)) { FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); fsd.setDrawable(icon); fsd.setScale(scale); icon = dr; scale = getScale(icon, outIconBounds); ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); } outScale[0] = scale; return icon; } /* Normalization block */ private static final float SCALE_NOT_INITIALIZED = 0; // 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 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 float mAdaptiveIconScale; private final Rect mAdaptiveIconBounds; private final Rect mBounds; private final int mMaxSize; private final byte[] mPixels; private final float[] mLeftBorder; private final float[] mRightBorder; private final Bitmap mBitmap; private final Canvas mScaleCheckCanvas; /** * 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} * * @param outBounds optional rect to receive the fraction distance from each edge. */ private synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds) { if (d instanceof AdaptiveIconDrawable) { if (mAdaptiveIconScale != SCALE_NOT_INITIALIZED) { if (outBounds != null) { outBounds.set(mAdaptiveIconBounds); } return mAdaptiveIconScale; } } 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(mScaleCheckCanvas); 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); } mBounds.left = leftX; mBounds.right = rightX; mBounds.top = topY; mBounds.bottom = bottomY; if (outBounds != null) { outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height, 1 - ((float) mBounds.right) / width, 1 - ((float) mBounds.bottom) / height); } 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 (d instanceof AdaptiveIconDrawable && mAdaptiveIconScale == SCALE_NOT_INITIALIZED) { mAdaptiveIconScale = scale; mAdaptiveIconBounds.set(mBounds); } return scale; } /** * Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values * (except on either ends) with appropriate values. * @param xCoordinates 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[] xCoordinates, int direction, int topY, int bottomY) { int total = xCoordinates.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 (xCoordinates[i] <= -1) { continue; } int start; if (lastAngle == Float.MAX_VALUE) { start = first; } else { float currentAngle = (xCoordinates[i] - xCoordinates[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 = (xCoordinates[i] - xCoordinates[start]) / (i - start); if ((currentAngle - angles[start]) * direction >= 0) { break; } } } } // Reset from last check lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start); // Update all the points from start. for (int j = start; j < i; j++) { angles[j] = lastAngle; xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start); } last = i; } } /* Shadow generator block */ private static final float KEY_SHADOW_DISTANCE = 1f / 48; private static final int KEY_SHADOW_ALPHA = 10; private static final int AMBIENT_SHADOW_ALPHA = 7; private Paint mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); private Paint mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); private BlurMaskFilter mDefaultBlurMaskFilter; private synchronized void recreateIcon(Bitmap icon, Canvas out) { recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out); } private synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter, int ambientAlpha, int keyAlpha, Canvas out) { int[] offset = new int[2]; mBlurPaint.setMaskFilter(blurMaskFilter); Bitmap shadow = icon.extractAlpha(mBlurPaint, offset); // Draw ambient shadow mDrawPaint.setAlpha(ambientAlpha); out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint); // Draw key shadow mDrawPaint.setAlpha(keyAlpha); out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconBitmapSize, mDrawPaint); // Draw the icon mDrawPaint.setAlpha(255); // TODO if b/128609682 not fixed by launch use .setAlpha(254) out.drawBitmap(icon, 0, 0, mDrawPaint); } /* Classes */ /** * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount. */ public static class FixedScaleDrawable extends DrawableWrapper { private static final float LEGACY_ICON_SCALE = .7f * .6667f; private float mScaleX, mScaleY; public FixedScaleDrawable() { super(new ColorDrawable()); mScaleX = LEGACY_ICON_SCALE; mScaleY = LEGACY_ICON_SCALE; } @Override public void draw(@NonNull Canvas canvas) { int saveCount = canvas.save(); canvas.scale(mScaleX, mScaleY, getBounds().exactCenterX(), getBounds().exactCenterY()); super.draw(canvas); canvas.restoreToCount(saveCount); } @Override public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { } @Override public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { } /** * Sets the scale associated with this drawable * @param scale */ public void setScale(float scale) { float h = getIntrinsicHeight(); float w = getIntrinsicWidth(); mScaleX = scale * LEGACY_ICON_SCALE; mScaleY = scale * LEGACY_ICON_SCALE; if (h > w && w > 0) { mScaleX *= w / h; } else if (w > h && h > 0) { mScaleY *= h / w; } } } /** * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size. * This allows the badging to be done based on the action bitmap size rather than * the scaled bitmap size. */ private static class FixedSizeBitmapDrawable extends BitmapDrawable { FixedSizeBitmapDrawable(Bitmap bitmap) { super(null, bitmap); } @Override public int getIntrinsicHeight() { return getBitmap().getWidth(); } @Override public int getIntrinsicWidth() { return getBitmap().getWidth(); } } }