1 package com.android.launcher3;
2 
3 import android.animation.ObjectAnimator;
4 import android.content.res.Resources.Theme;
5 import android.content.res.TypedArray;
6 import android.graphics.Canvas;
7 import android.graphics.Color;
8 import android.graphics.ColorFilter;
9 import android.graphics.Paint;
10 import android.graphics.PixelFormat;
11 import android.graphics.Rect;
12 import android.graphics.RectF;
13 import android.graphics.drawable.Drawable;
14 
15 class PreloadIconDrawable extends Drawable {
16 
17     private static final float ANIMATION_PROGRESS_STOPPED = -1.0f;
18     private static final float ANIMATION_PROGRESS_STARTED = 0f;
19     private static final float ANIMATION_PROGRESS_COMPLETED = 1.0f;
20 
21     private static final float MIN_SATUNATION = 0.2f;
22     private static final float MIN_LIGHTNESS = 0.6f;
23 
24     private static final float ICON_SCALE_FACTOR = 0.5f;
25     private static final int DEFAULT_COLOR = 0xFF009688;
26 
27     private static final Rect sTempRect = new Rect();
28 
29     private final RectF mIndicatorRect = new RectF();
30     private boolean mIndicatorRectDirty;
31 
32     private final Paint mPaint;
33     final Drawable mIcon;
34 
35     private Drawable mBgDrawable;
36     private int mRingOutset;
37 
38     private int mIndicatorColor = 0;
39 
40     /**
41      * Indicates the progress of the preloader [0-100]. If it goes above 100, only the icon
42      * is shown with no progress bar.
43      */
44     private int mProgress = 0;
45 
46     private float mAnimationProgress = ANIMATION_PROGRESS_STOPPED;
47     private ObjectAnimator mAnimator;
48 
PreloadIconDrawable(Drawable icon, Theme theme)49     public PreloadIconDrawable(Drawable icon, Theme theme) {
50         mIcon = icon;
51 
52         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
53         mPaint.setStyle(Paint.Style.STROKE);
54         mPaint.setStrokeCap(Paint.Cap.ROUND);
55 
56         setBounds(icon.getBounds());
57         applyTheme(theme);
58         onLevelChange(0);
59     }
60 
61     @Override
applyTheme(Theme t)62     public void applyTheme(Theme t) {
63         TypedArray ta = t.obtainStyledAttributes(R.styleable.PreloadIconDrawable);
64         mBgDrawable = ta.getDrawable(R.styleable.PreloadIconDrawable_background);
65         mBgDrawable.setFilterBitmap(true);
66         mPaint.setStrokeWidth(ta.getDimension(R.styleable.PreloadIconDrawable_indicatorSize, 0));
67         mRingOutset = ta.getDimensionPixelSize(R.styleable.PreloadIconDrawable_ringOutset, 0);
68         ta.recycle();
69         onBoundsChange(getBounds());
70         invalidateSelf();
71     }
72 
73     @Override
onBoundsChange(Rect bounds)74     protected void onBoundsChange(Rect bounds) {
75         mIcon.setBounds(bounds);
76         if (mBgDrawable != null) {
77             sTempRect.set(bounds);
78             sTempRect.inset(-mRingOutset, -mRingOutset);
79             mBgDrawable.setBounds(sTempRect);
80         }
81         mIndicatorRectDirty = true;
82     }
83 
getOutset()84     public int getOutset() {
85         return mRingOutset;
86     }
87 
88     /**
89      * The size of the indicator is same as the content region of the {@link #mBgDrawable} minus
90      * half the stroke size to accommodate the indicator.
91      */
initIndicatorRect()92     private void initIndicatorRect() {
93         Drawable d = mBgDrawable;
94         Rect bounds = d.getBounds();
95 
96         d.getPadding(sTempRect);
97         // Amount by which padding has to be scaled
98         float paddingScaleX = ((float) bounds.width()) / d.getIntrinsicWidth();
99         float paddingScaleY = ((float) bounds.height()) / d.getIntrinsicHeight();
100         mIndicatorRect.set(
101                 bounds.left + sTempRect.left * paddingScaleX,
102                 bounds.top + sTempRect.top * paddingScaleY,
103                 bounds.right - sTempRect.right * paddingScaleX,
104                 bounds.bottom - sTempRect.bottom * paddingScaleY);
105 
106         float inset = mPaint.getStrokeWidth() / 2;
107         mIndicatorRect.inset(inset, inset);
108         mIndicatorRectDirty = false;
109     }
110 
111     @Override
draw(Canvas canvas)112     public void draw(Canvas canvas) {
113         final Rect r = new Rect(getBounds());
114         if (canvas.getClipBounds(sTempRect) && !Rect.intersects(sTempRect, r)) {
115             // The draw region has been clipped.
116             return;
117         }
118         if (mIndicatorRectDirty) {
119             initIndicatorRect();
120         }
121         final float iconScale;
122 
123         if ((mAnimationProgress >= ANIMATION_PROGRESS_STARTED)
124                 && (mAnimationProgress < ANIMATION_PROGRESS_COMPLETED)) {
125             mPaint.setAlpha((int) ((1 - mAnimationProgress) * 255));
126             mBgDrawable.setAlpha(mPaint.getAlpha());
127             mBgDrawable.draw(canvas);
128             canvas.drawOval(mIndicatorRect, mPaint);
129 
130             iconScale = ICON_SCALE_FACTOR + (1 - ICON_SCALE_FACTOR) * mAnimationProgress;
131         } else if (mAnimationProgress == ANIMATION_PROGRESS_STOPPED) {
132             mPaint.setAlpha(255);
133             iconScale = ICON_SCALE_FACTOR;
134             mBgDrawable.setAlpha(255);
135             mBgDrawable.draw(canvas);
136 
137             if (mProgress >= 100) {
138                 canvas.drawOval(mIndicatorRect, mPaint);
139             } else if (mProgress > 0) {
140                 canvas.drawArc(mIndicatorRect, -90, mProgress * 3.6f, false, mPaint);
141             }
142         } else {
143             iconScale = 1;
144         }
145 
146         canvas.save();
147         canvas.scale(iconScale, iconScale, r.exactCenterX(), r.exactCenterY());
148         mIcon.draw(canvas);
149         canvas.restore();
150     }
151 
152     @Override
getOpacity()153     public int getOpacity() {
154         return PixelFormat.TRANSLUCENT;
155     }
156 
157     @Override
setAlpha(int alpha)158     public void setAlpha(int alpha) {
159         mIcon.setAlpha(alpha);
160     }
161 
162     @Override
setColorFilter(ColorFilter cf)163     public void setColorFilter(ColorFilter cf) {
164         mIcon.setColorFilter(cf);
165     }
166 
167     @Override
onLevelChange(int level)168     protected boolean onLevelChange(int level) {
169         mProgress = level;
170 
171         // Stop Animation
172         if (mAnimator != null) {
173             mAnimator.cancel();
174             mAnimator = null;
175         }
176         mAnimationProgress = ANIMATION_PROGRESS_STOPPED;
177         if (level > 0) {
178             // Set the paint color only when the level changes, so that the dominant color
179             // is only calculated when needed.
180             mPaint.setColor(getIndicatorColor());
181         }
182         if (mIcon instanceof FastBitmapDrawable) {
183             ((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0);
184         }
185 
186         invalidateSelf();
187         return true;
188     }
189 
190     /**
191      * Runs the finish animation if it is has not been run after last level change.
192      */
maybePerformFinishedAnimation()193     public void maybePerformFinishedAnimation() {
194         if (mAnimationProgress > ANIMATION_PROGRESS_STOPPED) {
195             return;
196         }
197         if (mAnimator != null) {
198             mAnimator.cancel();
199         }
200         setAnimationProgress(ANIMATION_PROGRESS_STARTED);
201         mAnimator = ObjectAnimator.ofFloat(this, "animationProgress",
202                 ANIMATION_PROGRESS_STARTED, ANIMATION_PROGRESS_COMPLETED);
203         mAnimator.start();
204     }
205 
setAnimationProgress(float progress)206     public void setAnimationProgress(float progress) {
207         if (progress != mAnimationProgress) {
208             mAnimationProgress = progress;
209             invalidateSelf();
210         }
211     }
212 
getAnimationProgress()213     public float getAnimationProgress() {
214         return mAnimationProgress;
215     }
216 
217     @Override
getIntrinsicHeight()218     public int getIntrinsicHeight() {
219         return mIcon.getIntrinsicHeight();
220     }
221 
222     @Override
getIntrinsicWidth()223     public int getIntrinsicWidth() {
224         return mIcon.getIntrinsicWidth();
225     }
226 
getIndicatorColor()227     private int getIndicatorColor() {
228         if (mIndicatorColor != 0) {
229             return mIndicatorColor;
230         }
231         if (!(mIcon instanceof FastBitmapDrawable)) {
232             mIndicatorColor = DEFAULT_COLOR;
233             return mIndicatorColor;
234         }
235         mIndicatorColor = Utilities.findDominantColorByHue(
236                 ((FastBitmapDrawable) mIcon).getBitmap(), 20);
237 
238         // Make sure that the dominant color has enough saturation to be visible properly.
239         float[] hsv = new float[3];
240         Color.colorToHSV(mIndicatorColor, hsv);
241         if (hsv[1] < MIN_SATUNATION) {
242             mIndicatorColor = DEFAULT_COLOR;
243             return mIndicatorColor;
244         }
245         hsv[2] = Math.max(MIN_LIGHTNESS, hsv[2]);
246         mIndicatorColor = Color.HSVToColor(hsv);
247         return mIndicatorColor;
248     }
249 }
250