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