• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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