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 import static com.android.launcher3.graphics.IconShape.getShapePath;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.ObjectAnimator;
26 import android.content.Context;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.Matrix;
30 import android.graphics.Paint;
31 import android.graphics.Path;
32 import android.graphics.PathMeasure;
33 import android.graphics.Rect;
34 import android.util.Pair;
35 import android.util.Property;
36 import android.util.SparseArray;
37 
38 import com.android.launcher3.FastBitmapDrawable;
39 import com.android.launcher3.anim.Interpolators;
40 import com.android.launcher3.model.data.ItemInfoWithIcon;
41 
42 import java.lang.ref.WeakReference;
43 
44 /**
45  * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
46  */
47 public class PreloadIconDrawable extends FastBitmapDrawable {
48 
49     private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
50             new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
51                 @Override
52                 public Float get(PreloadIconDrawable object) {
53                     return object.mInternalStateProgress;
54                 }
55 
56                 @Override
57                 public void set(PreloadIconDrawable object, Float value) {
58                     object.setInternalProgress(value);
59                 }
60             };
61 
62     private static final float PROGRESS_WIDTH = 7;
63     private static final float PROGRESS_GAP = 2;
64     private static final int MAX_PAINT_ALPHA = 255;
65 
66     private static final long DURATION_SCALE = 500;
67 
68     // The smaller the number, the faster the animation would be.
69     // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
70     private static final float COMPLETE_ANIM_FRACTION = 0.3f;
71 
72     private static final int COLOR_TRACK = 0x77EEEEEE;
73     private static final int COLOR_SHADOW = 0x55000000;
74 
75     private static final float SMALL_SCALE = 0.6f;
76 
77     private static final SparseArray<WeakReference<Pair<Path, Bitmap>>> sShadowCache =
78             new SparseArray<>();
79 
80     private final Matrix mTmpMatrix = new Matrix();
81     private final PathMeasure mPathMeasure = new PathMeasure();
82 
83     private final ItemInfoWithIcon mItem;
84 
85     // Path in [0, 100] bounds.
86     private final Path mShapePath;
87 
88     private final Path mScaledTrackPath;
89     private final Path mScaledProgressPath;
90     private final Paint mProgressPaint;
91 
92     private Bitmap mShadowBitmap;
93     private final int mIndicatorColor;
94 
95     private int mTrackAlpha;
96     private float mTrackLength;
97     private float mIconScale;
98 
99     private boolean mRanFinishAnimation;
100 
101     // Progress of the internal state. [0, 1] indicates the fraction of completed progress,
102     // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation.
103     private float mInternalStateProgress;
104 
105     private ObjectAnimator mCurrentAnim;
106 
PreloadIconDrawable(ItemInfoWithIcon info, Context context)107     public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
108         super(info.bitmap);
109         mItem = info;
110         mShapePath = getShapePath();
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         mShapePath.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<Pair<Path, Bitmap>> shadowRef = sShadowCache.get(key);
147         Pair<Path, Bitmap> cache = shadowRef != null ? shadowRef.get() : null;
148         Bitmap shadow = cache != null && cache.first.equals(mShapePath) ? cache.second : null;
149         if (shadow != null) {
150             return shadow;
151         }
152         shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
153         Canvas c = new Canvas(shadow);
154         mProgressPaint.setShadowLayer(shadowRadius, 0, 0, COLOR_SHADOW);
155         mProgressPaint.setColor(COLOR_TRACK);
156         mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
157         c.drawPath(mScaledTrackPath, mProgressPaint);
158         mProgressPaint.clearShadowLayer();
159         c.setBitmap(null);
160 
161         sShadowCache.put(key, new WeakReference<>(Pair.create(mShapePath, shadow)));
162         return shadow;
163     }
164 
165     @Override
drawInternal(Canvas canvas, Rect bounds)166     public void drawInternal(Canvas canvas, Rect bounds) {
167         if (mRanFinishAnimation) {
168             super.drawInternal(canvas, bounds);
169             return;
170         }
171 
172         // Draw track.
173         mProgressPaint.setColor(mIndicatorColor);
174         mProgressPaint.setAlpha(mTrackAlpha);
175         if (mShadowBitmap != null) {
176             canvas.drawBitmap(mShadowBitmap, bounds.left, bounds.top, mProgressPaint);
177         }
178         canvas.drawPath(mScaledProgressPath, mProgressPaint);
179 
180         int saveCount = canvas.save();
181         canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY());
182         super.drawInternal(canvas, bounds);
183         canvas.restoreToCount(saveCount);
184     }
185 
186     /**
187      * Updates the install progress based on the level
188      */
189     @Override
onLevelChange(int level)190     protected boolean onLevelChange(int level) {
191         // Run the animation if we have already been bound.
192         updateInternalState(level * 0.01f,  getBounds().width() > 0, false);
193         return true;
194     }
195 
196     /**
197      * Runs the finish animation if it is has not been run after last call to
198      * {@link #onLevelChange}
199      */
maybePerformFinishedAnimation()200     public void maybePerformFinishedAnimation() {
201         // If the drawable was recently initialized, skip the progress animation.
202         if (mInternalStateProgress == 0) {
203             mInternalStateProgress = 1;
204         }
205         updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true);
206     }
207 
hasNotCompleted()208     public boolean hasNotCompleted() {
209         return !mRanFinishAnimation;
210     }
211 
updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish)212     private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
213         if (mCurrentAnim != null) {
214             mCurrentAnim.cancel();
215             mCurrentAnim = null;
216         }
217 
218         if (Float.compare(finalProgress, mInternalStateProgress) == 0) {
219             return;
220         }
221         if (finalProgress < mInternalStateProgress) {
222             shouldAnimate = false;
223         }
224         if (!shouldAnimate || mRanFinishAnimation) {
225             setInternalProgress(finalProgress);
226         } else {
227             mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
228             mCurrentAnim.setDuration(
229                     (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
230             mCurrentAnim.setInterpolator(Interpolators.LINEAR);
231             if (isFinish) {
232                 mCurrentAnim.addListener(new AnimatorListenerAdapter() {
233                     @Override
234                     public void onAnimationEnd(Animator animation) {
235                         mRanFinishAnimation = true;
236                     }
237                 });
238             }
239             mCurrentAnim.start();
240         }
241     }
242 
243     /**
244      * Sets the internal progress and updates the UI accordingly
245      *   for progress <= 0:
246      *     - icon in the small scale and disabled state
247      *     - progress track is visible
248      *     - progress bar is not visible
249      *   for 0 < progress < 1
250      *     - icon in the small scale and disabled state
251      *     - progress track is visible
252      *     - progress bar is visible with dominant color. Progress bar is drawn as a fraction of
253      *       {@link #mScaledTrackPath}.
254      *       @see PathMeasure#getSegment(float, float, Path, boolean)
255      *   for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION)
256      *     - we calculate fraction of progress in the above range
257      *     - progress track is drawn with alpha based on fraction
258      *     - progress bar is drawn at 100% with alpha based on fraction
259      *     - icon is scaled up based on fraction and is drawn in enabled state
260      *   for progress >= (1 + COMPLETE_ANIM_FRACTION)
261      *     - only icon is drawn in normal state
262      */
setInternalProgress(float progress)263     private void setInternalProgress(float progress) {
264         mInternalStateProgress = progress;
265         if (progress <= 0) {
266             mIconScale = SMALL_SCALE;
267             mScaledTrackPath.reset();
268             mTrackAlpha = MAX_PAINT_ALPHA;
269             setIsDisabled(true);
270         }
271 
272         if (progress < 1 && progress > 0) {
273             mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
274             mIconScale = SMALL_SCALE;
275             mTrackAlpha = MAX_PAINT_ALPHA;
276             setIsDisabled(true);
277         } else if (progress >= 1) {
278             setIsDisabled(mItem.isDisabled());
279             mScaledTrackPath.set(mScaledProgressPath);
280             float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION;
281 
282             if (fraction >= 1) {
283                 // Animation has completed
284                 mIconScale = 1;
285                 mTrackAlpha = 0;
286             } else {
287                 mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA);
288                 mIconScale = SMALL_SCALE + (1 - SMALL_SCALE) * fraction;
289             }
290         }
291         invalidateSelf();
292     }
293 
294     /**
295      * Returns a FastBitmapDrawable with the icon.
296      */
newPendingIcon(Context context, ItemInfoWithIcon info)297     public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
298         return new PreloadIconDrawable(info, context);
299     }
300 }
301