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