1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 
18 package com.android.launcher3.graphics;
19 
20 import static com.android.launcher3.graphics.IconShape.DEFAULT_PATH_SIZE;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.ObjectAnimator;
25 import android.content.Context;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.Matrix;
29 import android.graphics.Paint;
30 import android.graphics.Path;
31 import android.graphics.PathMeasure;
32 import android.graphics.Rect;
33 import android.util.Property;
34 import android.util.SparseArray;
35 
36 import com.android.launcher3.FastBitmapDrawable;
37 import com.android.launcher3.ItemInfoWithIcon;
38 import com.android.launcher3.anim.Interpolators;
39 
40 import java.lang.ref.WeakReference;
41 
42 /**
43  * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
44  */
45 public class PreloadIconDrawable extends FastBitmapDrawable {
46 
47     private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
48             new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
49                 @Override
50                 public Float get(PreloadIconDrawable object) {
51                     return object.mInternalStateProgress;
52                 }
53 
54                 @Override
55                 public void set(PreloadIconDrawable object, Float value) {
56                     object.setInternalProgress(value);
57                 }
58             };
59 
60     private static final float PROGRESS_WIDTH = 7;
61     private static final float PROGRESS_GAP = 2;
62     private static final int MAX_PAINT_ALPHA = 255;
63 
64     private static final long DURATION_SCALE = 500;
65 
66     // The smaller the number, the faster the animation would be.
67     // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
68     private static final float COMPLETE_ANIM_FRACTION = 0.3f;
69 
70     private static final int COLOR_TRACK = 0x77EEEEEE;
71     private static final int COLOR_SHADOW = 0x55000000;
72 
73     private static final float SMALL_SCALE = 0.6f;
74 
75     private static final SparseArray<WeakReference<Bitmap>> sShadowCache = new SparseArray<>();
76 
77     private final Matrix mTmpMatrix = new Matrix();
78     private final PathMeasure mPathMeasure = new PathMeasure();
79 
80     private final ItemInfoWithIcon mItem;
81 
82     // Path in [0, 100] bounds.
83     private final Path mProgressPath;
84 
85     private final Path mScaledTrackPath;
86     private final Path mScaledProgressPath;
87     private final Paint mProgressPaint;
88 
89     private Bitmap mShadowBitmap;
90     private final int mIndicatorColor;
91 
92     private int mTrackAlpha;
93     private float mTrackLength;
94     private float mIconScale;
95 
96     private boolean mRanFinishAnimation;
97 
98     // Progress of the internal state. [0, 1] indicates the fraction of completed progress,
99     // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation.
100     private float mInternalStateProgress;
101 
102     private ObjectAnimator mCurrentAnim;
103 
104     /**
105      * @param progressPath fixed path in the bounds [0, 0, 100, 100] representing a progress bar.
106      */
PreloadIconDrawable(ItemInfoWithIcon info, Path progressPath, Context context)107     public PreloadIconDrawable(ItemInfoWithIcon info, Path progressPath, Context context) {
108         super(info);
109         mItem = info;
110         mProgressPath = progressPath;
111         mScaledTrackPath = new Path();
112         mScaledProgressPath = new Path();
113 
114         mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
115         mProgressPaint.setStyle(Paint.Style.STROKE);
116         mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
117         mIndicatorColor = IconPalette.getPreloadProgressColor(context, mIconColor);
118 
119         setInternalProgress(0);
120     }
121 
122     @Override
onBoundsChange(Rect bounds)123     protected void onBoundsChange(Rect bounds) {
124         super.onBoundsChange(bounds);
125         mTmpMatrix.setScale(
126                 (bounds.width() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / DEFAULT_PATH_SIZE,
127                 (bounds.height() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / DEFAULT_PATH_SIZE);
128         mTmpMatrix.postTranslate(
129                 bounds.left + PROGRESS_WIDTH + PROGRESS_GAP,
130                 bounds.top + PROGRESS_WIDTH + PROGRESS_GAP);
131 
132         mProgressPath.transform(mTmpMatrix, mScaledTrackPath);
133         float scale = bounds.width() / DEFAULT_PATH_SIZE;
134         mProgressPaint.setStrokeWidth(PROGRESS_WIDTH * scale);
135 
136         mShadowBitmap = getShadowBitmap(bounds.width(), bounds.height(),
137                 (PROGRESS_GAP ) * scale);
138         mPathMeasure.setPath(mScaledTrackPath, true);
139         mTrackLength = mPathMeasure.getLength();
140 
141         setInternalProgress(mInternalStateProgress);
142     }
143 
getShadowBitmap(int width, int height, float shadowRadius)144     private Bitmap getShadowBitmap(int width, int height, float shadowRadius) {
145         int key = (width << 16) | height;
146         WeakReference<Bitmap> shadowRef = sShadowCache.get(key);
147         Bitmap shadow = shadowRef != null ? shadowRef.get() : null;
148         if (shadow != null) {
149             return shadow;
150         }
151         shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
152         Canvas c = new Canvas(shadow);
153         mProgressPaint.setShadowLayer(shadowRadius, 0, 0, COLOR_SHADOW);
154         mProgressPaint.setColor(COLOR_TRACK);
155         mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
156         c.drawPath(mScaledTrackPath, mProgressPaint);
157         mProgressPaint.clearShadowLayer();
158         c.setBitmap(null);
159 
160         sShadowCache.put(key, new WeakReference<>(shadow));
161         return shadow;
162     }
163 
164     @Override
drawInternal(Canvas canvas, Rect bounds)165     public void drawInternal(Canvas canvas, Rect bounds) {
166         if (mRanFinishAnimation) {
167             super.drawInternal(canvas, bounds);
168             return;
169         }
170 
171         // Draw track.
172         mProgressPaint.setColor(mIndicatorColor);
173         mProgressPaint.setAlpha(mTrackAlpha);
174         if (mShadowBitmap != null) {
175             canvas.drawBitmap(mShadowBitmap, bounds.left, bounds.top, mProgressPaint);
176         }
177         canvas.drawPath(mScaledProgressPath, mProgressPaint);
178 
179         int saveCount = canvas.save();
180         canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY());
181         super.drawInternal(canvas, bounds);
182         canvas.restoreToCount(saveCount);
183     }
184 
185     /**
186      * Updates the install progress based on the level
187      */
188     @Override
onLevelChange(int level)189     protected boolean onLevelChange(int level) {
190         // Run the animation if we have already been bound.
191         updateInternalState(level * 0.01f,  getBounds().width() > 0, false);
192         return true;
193     }
194 
195     /**
196      * Runs the finish animation if it is has not been run after last call to
197      * {@link #onLevelChange}
198      */
maybePerformFinishedAnimation()199     public void maybePerformFinishedAnimation() {
200         // If the drawable was recently initialized, skip the progress animation.
201         if (mInternalStateProgress == 0) {
202             mInternalStateProgress = 1;
203         }
204         updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true);
205     }
206 
hasNotCompleted()207     public boolean hasNotCompleted() {
208         return !mRanFinishAnimation;
209     }
210 
updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish)211     private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
212         if (mCurrentAnim != null) {
213             mCurrentAnim.cancel();
214             mCurrentAnim = null;
215         }
216 
217         if (Float.compare(finalProgress, mInternalStateProgress) == 0) {
218             return;
219         }
220         if (finalProgress < mInternalStateProgress) {
221             shouldAnimate = false;
222         }
223         if (!shouldAnimate || mRanFinishAnimation) {
224             setInternalProgress(finalProgress);
225         } else {
226             mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
227             mCurrentAnim.setDuration(
228                     (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
229             mCurrentAnim.setInterpolator(Interpolators.LINEAR);
230             if (isFinish) {
231                 mCurrentAnim.addListener(new AnimatorListenerAdapter() {
232                     @Override
233                     public void onAnimationEnd(Animator animation) {
234                         mRanFinishAnimation = true;
235                     }
236                 });
237             }
238             mCurrentAnim.start();
239         }
240     }
241 
242     /**
243      * Sets the internal progress and updates the UI accordingly
244      *   for progress <= 0:
245      *     - icon in the small scale and disabled state
246      *     - progress track is visible
247      *     - progress bar is not visible
248      *   for 0 < progress < 1
249      *     - icon in the small scale and disabled state
250      *     - progress track is visible
251      *     - progress bar is visible with dominant color. Progress bar is drawn as a fraction of
252      *       {@link #mScaledTrackPath}.
253      *       @see PathMeasure#getSegment(float, float, Path, boolean)
254      *   for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION)
255      *     - we calculate fraction of progress in the above range
256      *     - progress track is drawn with alpha based on fraction
257      *     - progress bar is drawn at 100% with alpha based on fraction
258      *     - icon is scaled up based on fraction and is drawn in enabled state
259      *   for progress >= (1 + COMPLETE_ANIM_FRACTION)
260      *     - only icon is drawn in normal state
261      */
setInternalProgress(float progress)262     private void setInternalProgress(float progress) {
263         mInternalStateProgress = progress;
264         if (progress <= 0) {
265             mIconScale = SMALL_SCALE;
266             mScaledTrackPath.reset();
267             mTrackAlpha = MAX_PAINT_ALPHA;
268             setIsDisabled(true);
269         }
270 
271         if (progress < 1 && progress > 0) {
272             mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
273             mIconScale = SMALL_SCALE;
274             mTrackAlpha = MAX_PAINT_ALPHA;
275             setIsDisabled(true);
276         } else if (progress >= 1) {
277             setIsDisabled(mItem.isDisabled());
278             mScaledTrackPath.set(mScaledProgressPath);
279             float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION;
280 
281             if (fraction >= 1) {
282                 // Animation has completed
283                 mIconScale = 1;
284                 mTrackAlpha = 0;
285             } else {
286                 mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA);
287                 mIconScale = SMALL_SCALE + (1 - SMALL_SCALE) * fraction;
288             }
289         }
290         invalidateSelf();
291     }
292 }
293