• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ObjectAnimator;
23 import android.content.Context;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.Matrix;
27 import android.graphics.Paint;
28 import android.graphics.Path;
29 import android.graphics.PathMeasure;
30 import android.graphics.Rect;
31 import android.util.Pair;
32 import android.util.Property;
33 import android.util.SparseArray;
34 import android.view.ContextThemeWrapper;
35 
36 import com.android.launcher3.Utilities;
37 import com.android.launcher3.anim.Interpolators;
38 import com.android.launcher3.icons.FastBitmapDrawable;
39 import com.android.launcher3.icons.GraphicsUtils;
40 import com.android.launcher3.model.data.ItemInfoWithIcon;
41 import com.android.launcher3.util.Themes;
42 
43 import java.lang.ref.WeakReference;
44 
45 /**
46  * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
47  */
48 public class PreloadIconDrawable extends FastBitmapDrawable {
49 
50     private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
51             new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
52                 @Override
53                 public Float get(PreloadIconDrawable object) {
54                     return object.mInternalStateProgress;
55                 }
56 
57                 @Override
58                 public void set(PreloadIconDrawable object, Float value) {
59                     object.setInternalProgress(value);
60                 }
61             };
62 
63     private static final int DEFAULT_PATH_SIZE = 100;
64     private static final float PROGRESS_WIDTH = 7;
65     private static final float PROGRESS_GAP = 2;
66     private static final int MAX_PAINT_ALPHA = 255;
67 
68     private static final long DURATION_SCALE = 500;
69 
70     // The smaller the number, the faster the animation would be.
71     // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
72     private static final float COMPLETE_ANIM_FRACTION = 0.3f;
73 
74     private static final int COLOR_TRACK = 0x77EEEEEE;
75     private static final int COLOR_SHADOW = 0x55000000;
76 
77     private static final float SMALL_SCALE = 0.6f;
78 
79     private static final SparseArray<WeakReference<Pair<Path, Bitmap>>> sShadowCache =
80             new SparseArray<>();
81 
82     private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
83     private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;
84 
85     private final Matrix mTmpMatrix = new Matrix();
86     private final PathMeasure mPathMeasure = new PathMeasure();
87 
88     private final ItemInfoWithIcon mItem;
89 
90     // Path in [0, 100] bounds.
91     private final Path mShapePath;
92 
93     private final Path mScaledTrackPath;
94     private final Path mScaledProgressPath;
95     private final Paint mProgressPaint;
96 
97     private Bitmap mShadowBitmap;
98     private final int mIndicatorColor;
99     private final int mSystemAccentColor;
100     private final int mSystemBackgroundColor;
101     private final boolean mIsDarkMode;
102 
103     private int mTrackAlpha;
104     private float mTrackLength;
105     private float mIconScale;
106 
107     private boolean mRanFinishAnimation;
108 
109     // Progress of the internal state. [0, 1] indicates the fraction of completed progress,
110     // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation.
111     private float mInternalStateProgress;
112 
113     private ObjectAnimator mCurrentAnim;
114 
115     private boolean mIsStartable;
116 
PreloadIconDrawable(ItemInfoWithIcon info, Context context)117     public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
118         this(
119                 info,
120                 IconPalette.getPreloadProgressColor(context, info.bitmap.color),
121                 getPreloadColors(context),
122                 Utilities.isDarkTheme(context));
123     }
124 
PreloadIconDrawable( ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode)125     public PreloadIconDrawable(
126             ItemInfoWithIcon info,
127             int indicatorColor,
128             int[] preloadColors,
129             boolean isDarkMode) {
130         super(info.bitmap);
131         mItem = info;
132         mShapePath = GraphicsUtils.getShapePath(DEFAULT_PATH_SIZE);
133         mScaledTrackPath = new Path();
134         mScaledProgressPath = new Path();
135 
136         mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
137         mProgressPaint.setStyle(Paint.Style.STROKE);
138         mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
139         mIndicatorColor = indicatorColor;
140 
141         mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX];
142         mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX];
143         mIsDarkMode = isDarkMode;
144 
145         setInternalProgress(info.getProgressLevel());
146         setIsStartable(info.isAppStartable());
147     }
148 
149     @Override
onBoundsChange(Rect bounds)150     protected void onBoundsChange(Rect bounds) {
151         super.onBoundsChange(bounds);
152         mTmpMatrix.setScale(
153                 (bounds.width() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / DEFAULT_PATH_SIZE,
154                 (bounds.height() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / DEFAULT_PATH_SIZE);
155         mTmpMatrix.postTranslate(
156                 bounds.left + PROGRESS_WIDTH + PROGRESS_GAP,
157                 bounds.top + PROGRESS_WIDTH + PROGRESS_GAP);
158 
159         mShapePath.transform(mTmpMatrix, mScaledTrackPath);
160         float scale = bounds.width() / DEFAULT_PATH_SIZE;
161         mProgressPaint.setStrokeWidth(PROGRESS_WIDTH * scale);
162 
163         mShadowBitmap = getShadowBitmap(bounds.width(), bounds.height(),
164                 (PROGRESS_GAP ) * scale);
165         mPathMeasure.setPath(mScaledTrackPath, true);
166         mTrackLength = mPathMeasure.getLength();
167 
168         setInternalProgress(mInternalStateProgress);
169     }
170 
getShadowBitmap(int width, int height, float shadowRadius)171     private Bitmap getShadowBitmap(int width, int height, float shadowRadius) {
172         int key = ((width << 16) | height) * (mIsDarkMode ? -1 : 1);
173         WeakReference<Pair<Path, Bitmap>> shadowRef = sShadowCache.get(key);
174         Pair<Path, Bitmap> cache = shadowRef != null ? shadowRef.get() : null;
175         Bitmap shadow = cache != null && cache.first.equals(mShapePath) ? cache.second : null;
176         if (shadow != null) {
177             return shadow;
178         }
179         shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
180         Canvas c = new Canvas(shadow);
181         mProgressPaint.setShadowLayer(shadowRadius, 0, 0, mIsStartable
182                 ? COLOR_SHADOW : mSystemAccentColor);
183         mProgressPaint.setColor(mIsStartable ? COLOR_TRACK : mSystemBackgroundColor);
184         mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
185         c.drawPath(mScaledTrackPath, mProgressPaint);
186         mProgressPaint.clearShadowLayer();
187         c.setBitmap(null);
188 
189         sShadowCache.put(key, new WeakReference<>(Pair.create(mShapePath, shadow)));
190         return shadow;
191     }
192 
193     @Override
drawInternal(Canvas canvas, Rect bounds)194     public void drawInternal(Canvas canvas, Rect bounds) {
195         if (mRanFinishAnimation) {
196             super.drawInternal(canvas, bounds);
197             return;
198         }
199 
200         // Draw track.
201         mProgressPaint.setColor(mIsStartable ? mIndicatorColor : mSystemAccentColor);
202         mProgressPaint.setAlpha(mTrackAlpha);
203         if (mShadowBitmap != null) {
204             canvas.drawBitmap(mShadowBitmap, bounds.left, bounds.top, mProgressPaint);
205         }
206         canvas.drawPath(mScaledProgressPath, mProgressPaint);
207 
208         int saveCount = canvas.save();
209         canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY());
210         super.drawInternal(canvas, bounds);
211         canvas.restoreToCount(saveCount);
212     }
213 
214     /**
215      * Updates the install progress based on the level
216      */
217     @Override
onLevelChange(int level)218     protected boolean onLevelChange(int level) {
219         // Run the animation if we have already been bound.
220         updateInternalState(level * 0.01f,  getBounds().width() > 0, false);
221         return true;
222     }
223 
224     /**
225      * Runs the finish animation if it is has not been run after last call to
226      * {@link #onLevelChange}
227      */
maybePerformFinishedAnimation()228     public void maybePerformFinishedAnimation() {
229         // If the drawable was recently initialized, skip the progress animation.
230         if (mInternalStateProgress == 0) {
231             mInternalStateProgress = 1;
232         }
233         updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true);
234     }
235 
hasNotCompleted()236     public boolean hasNotCompleted() {
237         return !mRanFinishAnimation;
238     }
239 
240     /** Sets whether this icon should display the startable app UI. */
setIsStartable(boolean isStartable)241     public void setIsStartable(boolean isStartable) {
242         if (mIsStartable != isStartable) {
243             mIsStartable = isStartable;
244             setIsDisabled(!isStartable);
245         }
246     }
247 
updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish)248     private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
249         if (mCurrentAnim != null) {
250             mCurrentAnim.cancel();
251             mCurrentAnim = null;
252         }
253 
254         if (Float.compare(finalProgress, mInternalStateProgress) == 0) {
255             return;
256         }
257         if (finalProgress < mInternalStateProgress) {
258             shouldAnimate = false;
259         }
260         if (!shouldAnimate || mRanFinishAnimation) {
261             setInternalProgress(finalProgress);
262         } else {
263             mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
264             mCurrentAnim.setDuration(
265                     (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
266             mCurrentAnim.setInterpolator(Interpolators.LINEAR);
267             if (isFinish) {
268                 mCurrentAnim.addListener(new AnimatorListenerAdapter() {
269                     @Override
270                     public void onAnimationEnd(Animator animation) {
271                         mRanFinishAnimation = true;
272                     }
273                 });
274             }
275             mCurrentAnim.start();
276         }
277     }
278 
279     /**
280      * Sets the internal progress and updates the UI accordingly
281      *   for progress <= 0:
282      *     - icon in the small scale and disabled state
283      *     - progress track is visible
284      *     - progress bar is not visible
285      *   for 0 < progress < 1
286      *     - icon in the small scale and disabled state
287      *     - progress track is visible
288      *     - progress bar is visible with dominant color. Progress bar is drawn as a fraction of
289      *       {@link #mScaledTrackPath}.
290      *       @see PathMeasure#getSegment(float, float, Path, boolean)
291      *   for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION)
292      *     - we calculate fraction of progress in the above range
293      *     - progress track is drawn with alpha based on fraction
294      *     - progress bar is drawn at 100% with alpha based on fraction
295      *     - icon is scaled up based on fraction and is drawn in enabled state
296      *   for progress >= (1 + COMPLETE_ANIM_FRACTION)
297      *     - only icon is drawn in normal state
298      */
setInternalProgress(float progress)299     private void setInternalProgress(float progress) {
300         mInternalStateProgress = progress;
301         if (progress <= 0) {
302             mIconScale = SMALL_SCALE;
303             mScaledTrackPath.reset();
304             mTrackAlpha = MAX_PAINT_ALPHA;
305         }
306 
307         if (progress < 1 && progress > 0) {
308             mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
309             mIconScale = SMALL_SCALE;
310             mTrackAlpha = MAX_PAINT_ALPHA;
311         } else if (progress >= 1) {
312             setIsDisabled(mItem.isDisabled());
313             mScaledTrackPath.set(mScaledProgressPath);
314             float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION;
315 
316             if (fraction >= 1) {
317                 // Animation has completed
318                 mIconScale = 1;
319                 mTrackAlpha = 0;
320             } else {
321                 mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA);
322                 mIconScale = SMALL_SCALE + (1 - SMALL_SCALE) * fraction;
323             }
324         }
325         invalidateSelf();
326     }
327 
getPreloadColors(Context context)328     private static int[] getPreloadColors(Context context) {
329         Context dayNightThemeContext = new ContextThemeWrapper(
330                 context, android.R.style.Theme_DeviceDefault_DayNight);
331         int[] preloadColors = new int[2];
332 
333         preloadColors[PRELOAD_ACCENT_COLOR_INDEX] = Themes.getColorAccent(dayNightThemeContext);
334         preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX] = Themes.getColorBackgroundFloating(
335                 dayNightThemeContext);
336 
337         return preloadColors;
338     }
339 
340     /**
341      * Returns a FastBitmapDrawable with the icon.
342      */
newPendingIcon(Context context, ItemInfoWithIcon info)343     public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
344         return new PreloadIconDrawable(info, context);
345     }
346 
347     @Override
getConstantState()348     public ConstantState getConstantState() {
349         return new PreloadIconConstantState(
350                 mBitmap,
351                 mIconColor,
352                 !mItem.isAppStartable(),
353                 mItem,
354                 mIndicatorColor,
355                 new int[] {mSystemAccentColor, mSystemBackgroundColor},
356                 mIsDarkMode);
357     }
358 
359     protected static class PreloadIconConstantState extends FastBitmapConstantState {
360 
361         protected final ItemInfoWithIcon mInfo;
362         protected final int mIndicatorColor;
363         protected final int[] mPreloadColors;
364         protected final boolean mIsDarkMode;
365         protected final int mLevel;
366 
PreloadIconConstantState( Bitmap bitmap, int iconColor, boolean isDisabled, ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode)367         public PreloadIconConstantState(
368                 Bitmap bitmap,
369                 int iconColor,
370                 boolean isDisabled,
371                 ItemInfoWithIcon info,
372                 int indicatorColor,
373                 int[] preloadColors,
374                 boolean isDarkMode) {
375             super(bitmap, iconColor, isDisabled);
376             mInfo = info;
377             mIndicatorColor = indicatorColor;
378             mPreloadColors = preloadColors;
379             mIsDarkMode = isDarkMode;
380             mLevel = info.getProgressLevel();
381         }
382 
383         @Override
newDrawable()384         public PreloadIconDrawable newDrawable() {
385             return new PreloadIconDrawable(
386                     mInfo,
387                     mIndicatorColor,
388                     mPreloadColors,
389                     mIsDarkMode);
390         }
391 
392         @Override
getChangingConfigurations()393         public int getChangingConfigurations() {
394             return 0;
395         }
396     }
397 }
398