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         applyPreloaderTheme(theme);
58         onLevelChange(0);
59     }
60 
applyPreloaderTheme(Theme t)61     public void applyPreloaderTheme(Theme t) {
62         TypedArray ta = t.obtainStyledAttributes(R.styleable.PreloadIconDrawable);
63         mBgDrawable = ta.getDrawable(R.styleable.PreloadIconDrawable_background);
64         mBgDrawable.setFilterBitmap(true);
65         mPaint.setStrokeWidth(ta.getDimension(R.styleable.PreloadIconDrawable_indicatorSize, 0));
66         mRingOutset = ta.getDimensionPixelSize(R.styleable.PreloadIconDrawable_ringOutset, 0);
67         ta.recycle();
68         onBoundsChange(getBounds());
69         invalidateSelf();
70     }
71 
72     @Override
onBoundsChange(Rect bounds)73     protected void onBoundsChange(Rect bounds) {
74         mIcon.setBounds(bounds);
75         if (mBgDrawable != null) {
76             sTempRect.set(bounds);
77             sTempRect.inset(-mRingOutset, -mRingOutset);
78             mBgDrawable.setBounds(sTempRect);
79         }
80         mIndicatorRectDirty = true;
81     }
82 
getOutset()83     public int getOutset() {
84         return mRingOutset;
85     }
86 
87     /**
88      * The size of the indicator is same as the content region of the {@link #mBgDrawable} minus
89      * half the stroke size to accommodate the indicator.
90      */
initIndicatorRect()91     private void initIndicatorRect() {
92         Drawable d = mBgDrawable;
93         Rect bounds = d.getBounds();
94 
95         d.getPadding(sTempRect);
96         // Amount by which padding has to be scaled
97         float paddingScaleX = ((float) bounds.width()) / d.getIntrinsicWidth();
98         float paddingScaleY = ((float) bounds.height()) / d.getIntrinsicHeight();
99         mIndicatorRect.set(
100                 bounds.left + sTempRect.left * paddingScaleX,
101                 bounds.top + sTempRect.top * paddingScaleY,
102                 bounds.right - sTempRect.right * paddingScaleX,
103                 bounds.bottom - sTempRect.bottom * paddingScaleY);
104 
105         float inset = mPaint.getStrokeWidth() / 2;
106         mIndicatorRect.inset(inset, inset);
107         mIndicatorRectDirty = false;
108     }
109 
110     @Override
draw(Canvas canvas)111     public void draw(Canvas canvas) {
112         final Rect r = new Rect(getBounds());
113         if (canvas.getClipBounds(sTempRect) && !Rect.intersects(sTempRect, r)) {
114             // The draw region has been clipped.
115             return;
116         }
117         if (mIndicatorRectDirty) {
118             initIndicatorRect();
119         }
120         final float iconScale;
121 
122         if ((mAnimationProgress >= ANIMATION_PROGRESS_STARTED)
123                 && (mAnimationProgress < ANIMATION_PROGRESS_COMPLETED)) {
124             mPaint.setAlpha((int) ((1 - mAnimationProgress) * 255));
125             mBgDrawable.setAlpha(mPaint.getAlpha());
126             mBgDrawable.draw(canvas);
127             canvas.drawOval(mIndicatorRect, mPaint);
128 
129             iconScale = ICON_SCALE_FACTOR + (1 - ICON_SCALE_FACTOR) * mAnimationProgress;
130         } else if (mAnimationProgress == ANIMATION_PROGRESS_STOPPED) {
131             mPaint.setAlpha(255);
132             iconScale = ICON_SCALE_FACTOR;
133             mBgDrawable.setAlpha(255);
134             mBgDrawable.draw(canvas);
135 
136             if (mProgress >= 100) {
137                 canvas.drawOval(mIndicatorRect, mPaint);
138             } else if (mProgress > 0) {
139                 canvas.drawArc(mIndicatorRect, -90, mProgress * 3.6f, false, mPaint);
140             }
141         } else {
142             iconScale = 1;
143         }
144 
145         canvas.save();
146         canvas.scale(iconScale, iconScale, r.exactCenterX(), r.exactCenterY());
147         mIcon.draw(canvas);
148         canvas.restore();
149     }
150 
151     @Override
getOpacity()152     public int getOpacity() {
153         return PixelFormat.TRANSLUCENT;
154     }
155 
156     @Override
setAlpha(int alpha)157     public void setAlpha(int alpha) {
158         mIcon.setAlpha(alpha);
159     }
160 
161     @Override
setColorFilter(ColorFilter cf)162     public void setColorFilter(ColorFilter cf) {
163         mIcon.setColorFilter(cf);
164     }
165 
166     @Override
onLevelChange(int level)167     protected boolean onLevelChange(int level) {
168         mProgress = level;
169 
170         // Stop Animation
171         if (mAnimator != null) {
172             mAnimator.cancel();
173             mAnimator = null;
174         }
175         mAnimationProgress = ANIMATION_PROGRESS_STOPPED;
176         if (level > 0) {
177             // Set the paint color only when the level changes, so that the dominant color
178             // is only calculated when needed.
179             mPaint.setColor(getIndicatorColor());
180         }
181         if (mIcon instanceof FastBitmapDrawable) {
182             ((FastBitmapDrawable) mIcon).setState(level <= 0 ?
183                     FastBitmapDrawable.State.DISABLED : FastBitmapDrawable.State.NORMAL);
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 
hasNotCompleted()217     public boolean hasNotCompleted() {
218         return mAnimationProgress < ANIMATION_PROGRESS_COMPLETED;
219     }
220 
221     @Override
getIntrinsicHeight()222     public int getIntrinsicHeight() {
223         return mIcon.getIntrinsicHeight();
224     }
225 
226     @Override
getIntrinsicWidth()227     public int getIntrinsicWidth() {
228         return mIcon.getIntrinsicWidth();
229     }
230 
getIndicatorColor()231     private int getIndicatorColor() {
232         if (mIndicatorColor != 0) {
233             return mIndicatorColor;
234         }
235         if (!(mIcon instanceof FastBitmapDrawable)) {
236             mIndicatorColor = DEFAULT_COLOR;
237             return mIndicatorColor;
238         }
239         mIndicatorColor = Utilities.findDominantColorByHue(
240                 ((FastBitmapDrawable) mIcon).getBitmap(), 20);
241 
242         // Make sure that the dominant color has enough saturation to be visible properly.
243         float[] hsv = new float[3];
244         Color.colorToHSV(mIndicatorColor, hsv);
245         if (hsv[1] < MIN_SATUNATION) {
246             mIndicatorColor = DEFAULT_COLOR;
247             return mIndicatorColor;
248         }
249         hsv[2] = Math.max(MIN_LIGHTNESS, hsv[2]);
250         mIndicatorColor = Color.HSVToColor(hsv);
251         return mIndicatorColor;
252     }
253 }
254