1 package com.android.launcher3.icons; 2 3 import static android.graphics.Paint.ANTI_ALIAS_FLAG; 4 import static android.graphics.Paint.DITHER_FLAG; 5 import static android.graphics.Paint.FILTER_BITMAP_FLAG; 6 import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction; 7 8 import static com.android.launcher3.icons.BitmapInfo.FLAG_INSTANT; 9 import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR; 10 11 import static java.lang.annotation.RetentionPolicy.SOURCE; 12 13 import android.annotation.TargetApi; 14 import android.content.Context; 15 import android.content.Intent; 16 import android.content.pm.PackageManager; 17 import android.content.res.Resources; 18 import android.graphics.Bitmap; 19 import android.graphics.Bitmap.Config; 20 import android.graphics.Canvas; 21 import android.graphics.Color; 22 import android.graphics.Paint; 23 import android.graphics.PaintFlagsDrawFilter; 24 import android.graphics.Rect; 25 import android.graphics.RectF; 26 import android.graphics.drawable.AdaptiveIconDrawable; 27 import android.graphics.drawable.BitmapDrawable; 28 import android.graphics.drawable.ColorDrawable; 29 import android.graphics.drawable.Drawable; 30 import android.graphics.drawable.DrawableWrapper; 31 import android.graphics.drawable.InsetDrawable; 32 import android.os.Build; 33 import android.os.UserHandle; 34 import android.util.SparseArray; 35 36 import androidx.annotation.ColorInt; 37 import androidx.annotation.IntDef; 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 41 import com.android.launcher3.icons.BitmapInfo.Extender; 42 import com.android.launcher3.util.FlagOp; 43 import com.android.launcher3.util.UserIconInfo; 44 45 import java.lang.annotation.Retention; 46 import java.util.Objects; 47 48 /** 49 * This class will be moved to androidx library. There shouldn't be any dependency outside 50 * this package. 51 */ 52 public class BaseIconFactory implements AutoCloseable { 53 54 private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; 55 private static final float LEGACY_ICON_SCALE = .7f * (1f / (1 + 2 * getExtraInsetFraction())); 56 57 public static final int MODE_DEFAULT = 0; 58 public static final int MODE_ALPHA = 1; 59 public static final int MODE_WITH_SHADOW = 2; 60 public static final int MODE_HARDWARE = 3; 61 public static final int MODE_HARDWARE_WITH_SHADOW = 4; 62 63 @Retention(SOURCE) 64 @IntDef({MODE_DEFAULT, MODE_ALPHA, MODE_WITH_SHADOW, MODE_HARDWARE_WITH_SHADOW, MODE_HARDWARE}) 65 @interface BitmapGenerationMode { 66 } 67 68 private static final float ICON_BADGE_SCALE = 0.444f; 69 70 @NonNull 71 private final Rect mOldBounds = new Rect(); 72 73 @NonNull 74 private final SparseArray<UserIconInfo> mCachedUserInfo = new SparseArray<>(); 75 76 @NonNull 77 protected final Context mContext; 78 79 @NonNull 80 private final Canvas mCanvas; 81 82 @NonNull 83 private final PackageManager mPm; 84 85 @NonNull 86 private final ColorExtractor mColorExtractor; 87 88 protected final int mFillResIconDpi; 89 protected final int mIconBitmapSize; 90 91 protected boolean mMonoIconEnabled; 92 93 @Nullable 94 private IconNormalizer mNormalizer; 95 96 @Nullable 97 private ShadowGenerator mShadowGenerator; 98 99 private final boolean mShapeDetection; 100 101 // Shadow bitmap used as background for theme icons 102 private Bitmap mWhiteShadowLayer; 103 104 private Drawable mWrapperIcon; 105 private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; 106 107 private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(245, 245, 245); 108 BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, boolean shapeDetection)109 protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, 110 boolean shapeDetection) { 111 mContext = context.getApplicationContext(); 112 mShapeDetection = shapeDetection; 113 mFillResIconDpi = fillResIconDpi; 114 mIconBitmapSize = iconBitmapSize; 115 116 mPm = mContext.getPackageManager(); 117 mColorExtractor = new ColorExtractor(); 118 119 mCanvas = new Canvas(); 120 mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); 121 clear(); 122 } 123 BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize)124 public BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) { 125 this(context, fillResIconDpi, iconBitmapSize, false); 126 } 127 clear()128 protected void clear() { 129 mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; 130 } 131 132 @NonNull getShadowGenerator()133 public ShadowGenerator getShadowGenerator() { 134 if (mShadowGenerator == null) { 135 mShadowGenerator = new ShadowGenerator(mIconBitmapSize); 136 } 137 return mShadowGenerator; 138 } 139 140 @NonNull getNormalizer()141 public IconNormalizer getNormalizer() { 142 if (mNormalizer == null) { 143 mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection); 144 } 145 return mNormalizer; 146 } 147 148 @SuppressWarnings("deprecation") createIconBitmap(Intent.ShortcutIconResource iconRes)149 public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) { 150 try { 151 Resources resources = mPm.getResourcesForApplication(iconRes.packageName); 152 if (resources != null) { 153 final int id = resources.getIdentifier(iconRes.resourceName, null, null); 154 // do not stamp old legacy shortcuts as the app may have already forgotten about it 155 return createBadgedIconBitmap(resources.getDrawableForDensity(id, mFillResIconDpi)); 156 } 157 } catch (Exception e) { 158 // Icon not found. 159 } 160 return null; 161 } 162 163 /** 164 * Create a placeholder icon using the passed in text. 165 * 166 * @param placeholder used for foreground element in the icon bitmap 167 * @param color used for the foreground text color 168 */ createIconBitmap(String placeholder, int color)169 public BitmapInfo createIconBitmap(String placeholder, int color) { 170 AdaptiveIconDrawable drawable = new AdaptiveIconDrawable( 171 new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR), 172 new CenterTextDrawable(placeholder, color)); 173 Bitmap icon = createIconBitmap(drawable, IconNormalizer.ICON_VISIBLE_AREA_FACTOR); 174 return BitmapInfo.of(icon, color); 175 } 176 createIconBitmap(Bitmap icon)177 public BitmapInfo createIconBitmap(Bitmap icon) { 178 if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) { 179 icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f); 180 } 181 182 return BitmapInfo.of(icon, mColorExtractor.findDominantColorByHue(icon)); 183 } 184 185 /** 186 * Creates an icon from the bitmap cropped to the current device icon shape 187 */ 188 @NonNull createShapedIconBitmap(Bitmap icon, IconOptions options)189 public BitmapInfo createShapedIconBitmap(Bitmap icon, IconOptions options) { 190 Drawable d = new FixedSizeBitmapDrawable(icon); 191 float inset = getExtraInsetFraction(); 192 inset = inset / (1 + 2 * inset); 193 d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), 194 new InsetDrawable(d, inset, inset, inset, inset)); 195 return createBadgedIconBitmap(d, options); 196 } 197 198 @NonNull createBadgedIconBitmap(@onNull Drawable icon)199 public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon) { 200 return createBadgedIconBitmap(icon, null); 201 } 202 203 /** 204 * Creates bitmap using the source drawable and various parameters. 205 * The bitmap is visually normalized with other icons and has enough spacing to add shadow. 206 * 207 * @param icon source of the icon 208 * @return a bitmap suitable for disaplaying as an icon at various system UIs. 209 */ 210 @TargetApi(Build.VERSION_CODES.TIRAMISU) 211 @NonNull createBadgedIconBitmap(@onNull Drawable icon, @Nullable IconOptions options)212 public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, 213 @Nullable IconOptions options) { 214 float[] scale = new float[1]; 215 AdaptiveIconDrawable adaptiveIcon = normalizeAndWrapToAdaptiveIcon(icon, null, scale); 216 Bitmap bitmap = createIconBitmap(adaptiveIcon, scale[0], 217 options == null ? MODE_WITH_SHADOW : options.mGenerationMode); 218 219 int color = (options != null && options.mExtractedColor != null) 220 ? options.mExtractedColor : mColorExtractor.findDominantColorByHue(bitmap); 221 BitmapInfo info = BitmapInfo.of(bitmap, color); 222 223 if (adaptiveIcon instanceof BitmapInfo.Extender extender) { 224 info = extender.getExtendedInfo(bitmap, color, this, scale[0]); 225 } else if (IconProvider.ATLEAST_T && mMonoIconEnabled) { 226 Drawable mono = getMonochromeDrawable(adaptiveIcon); 227 if (mono != null) { 228 info.setMonoIcon(createIconBitmap(mono, scale[0], MODE_ALPHA), this); 229 } 230 } 231 info = info.withFlags(getBitmapFlagOp(options)); 232 return info; 233 } 234 235 /** 236 * Returns a monochromatic version of the given drawable or null, if it is not supported 237 * 238 * @param base the original icon 239 */ 240 @TargetApi(Build.VERSION_CODES.TIRAMISU) getMonochromeDrawable(AdaptiveIconDrawable base)241 protected Drawable getMonochromeDrawable(AdaptiveIconDrawable base) { 242 Drawable mono = base.getMonochrome(); 243 if (mono != null) { 244 return new ClippedMonoDrawable(mono); 245 } 246 return null; 247 } 248 249 @NonNull getBitmapFlagOp(@ullable IconOptions options)250 public FlagOp getBitmapFlagOp(@Nullable IconOptions options) { 251 FlagOp op = FlagOp.NO_OP; 252 if (options != null) { 253 if (options.mIsInstantApp) { 254 op = op.addFlag(FLAG_INSTANT); 255 } 256 257 UserIconInfo info = options.mUserIconInfo; 258 if (info == null && options.mUserHandle != null) { 259 info = getUserInfo(options.mUserHandle); 260 } 261 if (info != null) { 262 op = info.applyBitmapInfoFlags(op); 263 } 264 } 265 return op; 266 } 267 268 @NonNull getUserInfo(@onNull UserHandle user)269 protected UserIconInfo getUserInfo(@NonNull UserHandle user) { 270 int key = user.hashCode(); 271 UserIconInfo info = mCachedUserInfo.get(key); 272 /* 273 * We do not have the ability to distinguish between different badged users here. 274 * As such all badged users will have the work profile badge applied. 275 */ 276 if (info == null) { 277 // Simple check to check if the provided user is work profile or not based on badging 278 NoopDrawable d = new NoopDrawable(); 279 boolean isWork = (d != mPm.getUserBadgedIcon(d, user)); 280 info = new UserIconInfo(user, isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN); 281 mCachedUserInfo.put(key, info); 282 } 283 return info; 284 } 285 286 @NonNull getWhiteShadowLayer()287 public Bitmap getWhiteShadowLayer() { 288 if (mWhiteShadowLayer == null) { 289 mWhiteShadowLayer = createScaledBitmap( 290 new AdaptiveIconDrawable(new ColorDrawable(Color.WHITE), null), 291 MODE_HARDWARE_WITH_SHADOW); 292 } 293 return mWhiteShadowLayer; 294 } 295 296 @NonNull createScaledBitmap(@onNull Drawable icon, @BitmapGenerationMode int mode)297 public Bitmap createScaledBitmap(@NonNull Drawable icon, @BitmapGenerationMode int mode) { 298 RectF iconBounds = new RectF(); 299 float[] scale = new float[1]; 300 icon = normalizeAndWrapToAdaptiveIcon(icon, iconBounds, scale); 301 return createIconBitmap(icon, 302 Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)), mode); 303 } 304 305 /** 306 * Sets the background color used for wrapped adaptive icon 307 */ setWrapperBackgroundColor(final int color)308 public void setWrapperBackgroundColor(final int color) { 309 mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color; 310 } 311 312 @Nullable normalizeAndWrapToAdaptiveIcon(@ullable Drawable icon, @Nullable final RectF outIconBounds, @NonNull final float[] outScale)313 protected AdaptiveIconDrawable normalizeAndWrapToAdaptiveIcon(@Nullable Drawable icon, 314 @Nullable final RectF outIconBounds, @NonNull final float[] outScale) { 315 if (icon == null) { 316 return null; 317 } 318 319 AdaptiveIconDrawable adaptiveIcon; 320 float scale; 321 adaptiveIcon = wrapToAdaptiveIcon(icon, outIconBounds); 322 scale = getNormalizer().getScale(adaptiveIcon, outIconBounds, null, null); 323 outScale[0] = scale; 324 return adaptiveIcon; 325 } 326 327 /** 328 * Returns a drawable which draws the original drawable at a fixed scale 329 */ createScaledDrawable(@onNull Drawable main, float scale)330 private Drawable createScaledDrawable(@NonNull Drawable main, float scale) { 331 float h = main.getIntrinsicHeight(); 332 float w = main.getIntrinsicWidth(); 333 float scaleX = scale; 334 float scaleY = scale; 335 if (h > w && w > 0) { 336 scaleX *= w / h; 337 } else if (w > h && h > 0) { 338 scaleY *= h / w; 339 } 340 scaleX = (1 - scaleX) / 2; 341 scaleY = (1 - scaleY) / 2; 342 return new InsetDrawable(main, scaleX, scaleY, scaleX, scaleY); 343 } 344 345 /** 346 * Wraps the provided icon in an adaptive icon drawable 347 */ wrapToAdaptiveIcon(@onNull Drawable icon, @Nullable final RectF outIconBounds)348 public AdaptiveIconDrawable wrapToAdaptiveIcon(@NonNull Drawable icon, 349 @Nullable final RectF outIconBounds) { 350 if (icon instanceof AdaptiveIconDrawable aid) { 351 return aid; 352 } else { 353 EmptyWrapper foreground = new EmptyWrapper(); 354 AdaptiveIconDrawable dr = new AdaptiveIconDrawable( 355 new ColorDrawable(mWrapperBackgroundColor), foreground); 356 dr.setBounds(0, 0, 1, 1); 357 boolean[] outShape = new boolean[1]; 358 float scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape); 359 if (!outShape[0]) { 360 foreground.setDrawable(createScaledDrawable(icon, scale * LEGACY_ICON_SCALE)); 361 } else { 362 foreground.setDrawable(createScaledDrawable(icon, 1 - getExtraInsetFraction())); 363 } 364 return dr; 365 } 366 } 367 368 @NonNull createIconBitmap(@ullable final Drawable icon, final float scale)369 protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale) { 370 return createIconBitmap(icon, scale, MODE_DEFAULT); 371 } 372 373 @NonNull createIconBitmap(@ullable final Drawable icon, final float scale, @BitmapGenerationMode int bitmapGenerationMode)374 protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale, 375 @BitmapGenerationMode int bitmapGenerationMode) { 376 final int size = mIconBitmapSize; 377 final Bitmap bitmap; 378 switch (bitmapGenerationMode) { 379 case MODE_ALPHA: 380 bitmap = Bitmap.createBitmap(size, size, Config.ALPHA_8); 381 break; 382 case MODE_HARDWARE: 383 case MODE_HARDWARE_WITH_SHADOW: { 384 return BitmapRenderer.createHardwareBitmap(size, size, canvas -> 385 drawIconBitmap(canvas, icon, scale, bitmapGenerationMode, null)); 386 } 387 case MODE_WITH_SHADOW: 388 default: 389 bitmap = Bitmap.createBitmap(size, size, Config.ARGB_8888); 390 break; 391 } 392 if (icon == null) { 393 return bitmap; 394 } 395 mCanvas.setBitmap(bitmap); 396 drawIconBitmap(mCanvas, icon, scale, bitmapGenerationMode, bitmap); 397 mCanvas.setBitmap(null); 398 return bitmap; 399 } 400 drawIconBitmap(@onNull Canvas canvas, @Nullable final Drawable icon, final float scale, @BitmapGenerationMode int bitmapGenerationMode, @Nullable Bitmap targetBitmap)401 private void drawIconBitmap(@NonNull Canvas canvas, @Nullable final Drawable icon, 402 final float scale, @BitmapGenerationMode int bitmapGenerationMode, 403 @Nullable Bitmap targetBitmap) { 404 final int size = mIconBitmapSize; 405 mOldBounds.set(icon.getBounds()); 406 407 if (icon instanceof AdaptiveIconDrawable) { 408 // We are ignoring KEY_SHADOW_DISTANCE because regular icons ignore this at the 409 // moment b/298203449 410 int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size), 411 Math.round(size * (1 - scale) / 2)); 412 // b/211896569: AdaptiveIconDrawable do not work properly for non top-left bounds 413 icon.setBounds(0, 0, size - offset - offset, size - offset - offset); 414 int count = canvas.save(); 415 canvas.translate(offset, offset); 416 if (bitmapGenerationMode == MODE_WITH_SHADOW 417 || bitmapGenerationMode == MODE_HARDWARE_WITH_SHADOW) { 418 getShadowGenerator().addPathShadow( 419 ((AdaptiveIconDrawable) icon).getIconMask(), canvas); 420 } 421 422 if (icon instanceof BitmapInfo.Extender) { 423 ((Extender) icon).drawForPersistence(canvas); 424 } else { 425 icon.draw(canvas); 426 } 427 canvas.restoreToCount(count); 428 } else { 429 if (icon instanceof BitmapDrawable) { 430 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; 431 Bitmap b = bitmapDrawable.getBitmap(); 432 if (b != null && b.getDensity() == Bitmap.DENSITY_NONE) { 433 bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics()); 434 } 435 } 436 int width = size; 437 int height = size; 438 439 int intrinsicWidth = icon.getIntrinsicWidth(); 440 int intrinsicHeight = icon.getIntrinsicHeight(); 441 if (intrinsicWidth > 0 && intrinsicHeight > 0) { 442 // Scale the icon proportionally to the icon dimensions 443 final float ratio = (float) intrinsicWidth / intrinsicHeight; 444 if (intrinsicWidth > intrinsicHeight) { 445 height = (int) (width / ratio); 446 } else if (intrinsicHeight > intrinsicWidth) { 447 width = (int) (height * ratio); 448 } 449 } 450 final int left = (size - width) / 2; 451 final int top = (size - height) / 2; 452 icon.setBounds(left, top, left + width, top + height); 453 454 canvas.save(); 455 canvas.scale(scale, scale, size / 2, size / 2); 456 icon.draw(canvas); 457 canvas.restore(); 458 459 if (bitmapGenerationMode == MODE_WITH_SHADOW && targetBitmap != null) { 460 // Shadow extraction only works in software mode 461 getShadowGenerator().drawShadow(targetBitmap, canvas); 462 463 // Draw the icon again on top: 464 canvas.save(); 465 canvas.scale(scale, scale, size / 2, size / 2); 466 icon.draw(canvas); 467 canvas.restore(); 468 } 469 } 470 icon.setBounds(mOldBounds); 471 } 472 473 @Override close()474 public void close() { 475 clear(); 476 } 477 478 @NonNull makeDefaultIcon()479 public BitmapInfo makeDefaultIcon() { 480 return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi)); 481 } 482 483 @NonNull getFullResDefaultActivityIcon(final int iconDpi)484 public static Drawable getFullResDefaultActivityIcon(final int iconDpi) { 485 return Objects.requireNonNull(Resources.getSystem().getDrawableForDensity( 486 android.R.drawable.sym_def_app_icon, iconDpi)); 487 } 488 489 /** 490 * Returns the correct badge size given an icon size 491 */ getBadgeSizeForIconSize(final int iconSize)492 public static int getBadgeSizeForIconSize(final int iconSize) { 493 return (int) (ICON_BADGE_SCALE * iconSize); 494 } 495 496 public static class IconOptions { 497 498 boolean mIsInstantApp; 499 500 @BitmapGenerationMode 501 int mGenerationMode = MODE_WITH_SHADOW; 502 503 @Nullable 504 UserHandle mUserHandle; 505 @Nullable 506 UserIconInfo mUserIconInfo; 507 508 @ColorInt 509 @Nullable 510 Integer mExtractedColor; 511 512 513 /** 514 * User for this icon, in case of badging 515 */ 516 @NonNull setUser(@ullable final UserHandle user)517 public IconOptions setUser(@Nullable final UserHandle user) { 518 mUserHandle = user; 519 return this; 520 } 521 522 /** 523 * User for this icon, in case of badging 524 */ 525 @NonNull setUser(@ullable final UserIconInfo user)526 public IconOptions setUser(@Nullable final UserIconInfo user) { 527 mUserIconInfo = user; 528 return this; 529 } 530 531 /** 532 * If this icon represents an instant app 533 */ 534 @NonNull setInstantApp(final boolean instantApp)535 public IconOptions setInstantApp(final boolean instantApp) { 536 mIsInstantApp = instantApp; 537 return this; 538 } 539 540 /** 541 * Disables auto color extraction and overrides the color to the provided value 542 */ 543 @NonNull setExtractedColor(@olorInt int color)544 public IconOptions setExtractedColor(@ColorInt int color) { 545 mExtractedColor = color; 546 return this; 547 } 548 549 /** 550 * Sets the bitmap generation mode to use for the bitmap info. Note that some generation 551 * modes do not support color extraction, so consider setting a extracted color manually 552 * in those cases. 553 */ setBitmapGenerationMode(@itmapGenerationMode int generationMode)554 public IconOptions setBitmapGenerationMode(@BitmapGenerationMode int generationMode) { 555 mGenerationMode = generationMode; 556 return this; 557 } 558 } 559 560 /** 561 * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size. 562 * This allows the badging to be done based on the action bitmap size rather than 563 * the scaled bitmap size. 564 */ 565 private static class FixedSizeBitmapDrawable extends BitmapDrawable { 566 FixedSizeBitmapDrawable(@ullable final Bitmap bitmap)567 public FixedSizeBitmapDrawable(@Nullable final Bitmap bitmap) { 568 super(null, bitmap); 569 } 570 571 @Override getIntrinsicHeight()572 public int getIntrinsicHeight() { 573 return getBitmap().getWidth(); 574 } 575 576 @Override getIntrinsicWidth()577 public int getIntrinsicWidth() { 578 return getBitmap().getWidth(); 579 } 580 } 581 582 private static class NoopDrawable extends ColorDrawable { 583 @Override getIntrinsicHeight()584 public int getIntrinsicHeight() { 585 return 1; 586 } 587 588 @Override getIntrinsicWidth()589 public int getIntrinsicWidth() { 590 return 1; 591 } 592 } 593 594 protected static class ClippedMonoDrawable extends InsetDrawable { 595 596 @NonNull 597 private final AdaptiveIconDrawable mCrop; 598 ClippedMonoDrawable(@ullable final Drawable base)599 public ClippedMonoDrawable(@Nullable final Drawable base) { 600 super(base, -getExtraInsetFraction()); 601 mCrop = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null); 602 } 603 604 @Override draw(Canvas canvas)605 public void draw(Canvas canvas) { 606 mCrop.setBounds(getBounds()); 607 int saveCount = canvas.save(); 608 canvas.clipPath(mCrop.getIconMask()); 609 super.draw(canvas); 610 canvas.restoreToCount(saveCount); 611 } 612 } 613 614 private static class CenterTextDrawable extends ColorDrawable { 615 616 @NonNull 617 private final Rect mTextBounds = new Rect(); 618 619 @NonNull 620 private final Paint mTextPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); 621 622 @NonNull 623 private final String mText; 624 CenterTextDrawable(@onNull final String text, final int color)625 CenterTextDrawable(@NonNull final String text, final int color) { 626 mText = text; 627 mTextPaint.setColor(color); 628 } 629 630 @Override draw(Canvas canvas)631 public void draw(Canvas canvas) { 632 Rect bounds = getBounds(); 633 mTextPaint.setTextSize(bounds.height() / 3f); 634 mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds); 635 canvas.drawText(mText, 636 bounds.exactCenterX() - mTextBounds.exactCenterX(), 637 bounds.exactCenterY() - mTextBounds.exactCenterY(), 638 mTextPaint); 639 } 640 } 641 642 private static class EmptyWrapper extends DrawableWrapper { 643 EmptyWrapper()644 EmptyWrapper() { 645 super(new ColorDrawable()); 646 } 647 648 @Override getConstantState()649 public ConstantState getConstantState() { 650 Drawable d = getDrawable(); 651 return d == null ? null : d.getConstantState(); 652 } 653 } 654 } 655