• 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 static com.android.app.animation.Interpolators.EMPHASIZED;
21 import static com.android.app.animation.Interpolators.LINEAR;
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.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 
35 import androidx.annotation.VisibleForTesting;
36 import androidx.core.graphics.ColorUtils;
37 
38 import com.android.launcher3.R;
39 import com.android.launcher3.Utilities;
40 import com.android.launcher3.anim.AnimatedFloat;
41 import com.android.launcher3.anim.AnimatorListeners;
42 import com.android.launcher3.icons.BitmapInfo;
43 import com.android.launcher3.icons.FastBitmapDrawable;
44 import com.android.launcher3.model.data.ItemInfoWithIcon;
45 import com.android.launcher3.util.Themes;
46 
47 /**
48  * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
49  */
50 public class PreloadIconDrawable extends FastBitmapDrawable {
51 
52     private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
53             new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
54                 @Override
55                 public Float get(PreloadIconDrawable object) {
56                     return object.mInternalStateProgress;
57                 }
58 
59                 @Override
60                 public void set(PreloadIconDrawable object, Float value) {
61                     object.setInternalProgress(value);
62                 }
63             };
64 
65     private static final int DEFAULT_PATH_SIZE = 100;
66     private static final int MAX_PAINT_ALPHA = 255;
67 
68     private static final long DURATION_SCALE = 500;
69     private static final long SCALE_AND_ALPHA_ANIM_DURATION = 500;
70 
71     // The smaller the number, the faster the animation would be.
72     // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
73     private static final float COMPLETE_ANIM_FRACTION = 1f;
74 
75     private static final float SMALL_SCALE = 0.8f;
76     private static final float PROGRESS_STROKE_SCALE = 0.055f;
77     private static final float PROGRESS_BOUNDS_SCALE = 0.075f;
78     private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
79     private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;
80 
81     private final Matrix mTmpMatrix = new Matrix();
82     private final PathMeasure mPathMeasure = new PathMeasure();
83 
84     private final ItemInfoWithIcon mItem;
85 
86     // Path in [0, 100] bounds.
87     private final Path mShapePath;
88 
89     private final Path mScaledTrackPath;
90     private final Path mScaledProgressPath;
91     private final Paint mProgressPaint;
92 
93     private final int mIndicatorColor;
94     private final int mSystemAccentColor;
95     private final int mSystemBackgroundColor;
96 
97     private int mProgressColor;
98     private int mTrackColor;
99     private int mPlateColor;
100 
101     private final boolean mIsDarkMode;
102 
103     private float mTrackLength;
104 
105     private boolean mRanFinishAnimation;
106 
107     // Progress of the internal state. [0, 1] indicates the fraction of completed progress,
108     // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation.
109     private float mInternalStateProgress;
110     // This multiplier is used to animate scale when going from 0 to non-zero and expanding
111     private final Runnable mInvalidateRunnable = this::invalidateSelf;
112     private final AnimatedFloat mIconScaleMultiplier = new AnimatedFloat(mInvalidateRunnable);
113 
114     private ObjectAnimator mCurrentAnim;
115 
PreloadIconDrawable(ItemInfoWithIcon info, Context context)116     public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
117         this(
118                 info,
119                 IconPalette.getPreloadProgressColor(context, info.bitmap.color),
120                 getPreloadColors(context),
121                 Utilities.isDarkTheme(context),
122                 ThemeManager.INSTANCE.get(context).getIconShape().getPath(DEFAULT_PATH_SIZE)
123         );
124     }
125 
PreloadIconDrawable( ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode, Path shapePath)126     public PreloadIconDrawable(
127             ItemInfoWithIcon info,
128             int indicatorColor,
129             int[] preloadColors,
130             boolean isDarkMode,
131             Path shapePath) {
132         super(info.bitmap);
133         mItem = info;
134         mShapePath = shapePath;
135         mScaledTrackPath = new Path();
136         mScaledProgressPath = new Path();
137 
138         mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
139         mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
140         mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
141         mIndicatorColor = indicatorColor;
142 
143         // This is the color
144         int primaryIconColor = mItem.bitmap.color;
145 
146         // Progress color
147         float[] m3HCT = new float[3];
148         ColorUtils.colorToM3HCT(primaryIconColor, m3HCT);
149         mProgressColor = ColorUtils.M3HCTToColor(
150                 m3HCT[0],
151                 m3HCT[1],
152                 isDarkMode ? Math.max(m3HCT[2], 55) : Math.min(m3HCT[2], 40));
153 
154         // Track color
155         mTrackColor = ColorUtils.M3HCTToColor(
156                 m3HCT[0],
157                 16,
158                 isDarkMode ? 30 : 90
159         );
160         // Plate color
161         mPlateColor = ColorUtils.M3HCTToColor(
162                 m3HCT[0],
163                 isDarkMode ? 36 : 24,
164                 isDarkMode ? (isThemed() ? 10 : 20) : 80
165         );
166 
167         mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX];
168         mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX];
169         mIsDarkMode = isDarkMode;
170 
171         // If it's a pending app we will animate scale and alpha when it's no longer pending.
172         mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1);
173 
174         setLevel(info.getProgressLevel());
175         // Set a disabled icon color if the app is suspended or if the app is pending download
176         setIsDisabled(info.isDisabled() || info.isPendingDownload());
177     }
178 
179     @Override
onBoundsChange(Rect bounds)180     protected void onBoundsChange(Rect bounds) {
181         super.onBoundsChange(bounds);
182 
183         float progressWidth = bounds.width() * PROGRESS_BOUNDS_SCALE;
184         mTmpMatrix.setScale(
185                 (bounds.width() - 2 * progressWidth) / DEFAULT_PATH_SIZE,
186                 (bounds.height() - 2 * progressWidth) / DEFAULT_PATH_SIZE);
187         mTmpMatrix.postTranslate(bounds.left + progressWidth, bounds.top + progressWidth);
188 
189         mShapePath.transform(mTmpMatrix, mScaledTrackPath);
190         mProgressPaint.setStrokeWidth(PROGRESS_STROKE_SCALE * bounds.width());
191 
192         mPathMeasure.setPath(mScaledTrackPath, true);
193         mTrackLength = mPathMeasure.getLength();
194 
195         setInternalProgress(mInternalStateProgress);
196     }
197 
198     @Override
drawInternal(Canvas canvas, Rect bounds)199     public void drawInternal(Canvas canvas, Rect bounds) {
200         if (mRanFinishAnimation) {
201             super.drawInternal(canvas, bounds);
202             return;
203         }
204 
205         if (mInternalStateProgress > 0) {
206             // Draw background.
207             mProgressPaint.setStyle(Paint.Style.FILL);
208             mProgressPaint.setColor(mPlateColor);
209             canvas.drawPath(mScaledTrackPath, mProgressPaint);
210         }
211 
212         if (mInternalStateProgress > 0) {
213             // Draw track and progress.
214             mProgressPaint.setStyle(Paint.Style.STROKE);
215             mProgressPaint.setColor(mTrackColor);
216             canvas.drawPath(mScaledTrackPath, mProgressPaint);
217             mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
218             mProgressPaint.setColor(mProgressColor);
219             canvas.drawPath(mScaledProgressPath, mProgressPaint);
220         }
221 
222         int saveCount = canvas.save();
223         float scale = 1 - mIconScaleMultiplier.value * (1 - SMALL_SCALE);
224         canvas.scale(scale, scale, bounds.exactCenterX(), bounds.exactCenterY());
225 
226         super.drawInternal(canvas, bounds);
227         canvas.restoreToCount(saveCount);
228     }
229 
230     /**
231      * Updates the install progress based on the level
232      */
233     @Override
onLevelChange(int level)234     protected boolean onLevelChange(int level) {
235         // Run the animation if we have already been bound.
236         updateInternalState(level * 0.01f, false, null);
237         return true;
238     }
239 
240     /**
241      * Runs the finish animation if it is has not been run after last call to
242      * {@link #onLevelChange}
243      */
maybePerformFinishedAnimation( PreloadIconDrawable oldIcon, Runnable onFinishCallback)244     public void maybePerformFinishedAnimation(
245             PreloadIconDrawable oldIcon, Runnable onFinishCallback) {
246 
247         mProgressColor = oldIcon.mProgressColor;
248         mTrackColor = oldIcon.mTrackColor;
249         mPlateColor = oldIcon.mPlateColor;
250 
251         if (oldIcon.mInternalStateProgress >= 1) {
252             mInternalStateProgress = oldIcon.mInternalStateProgress;
253         }
254 
255         // If the drawable was recently initialized, skip the progress animation.
256         if (mInternalStateProgress == 0) {
257             mInternalStateProgress = 1;
258         }
259         updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, onFinishCallback);
260     }
261 
hasNotCompleted()262     public boolean hasNotCompleted() {
263         return !mRanFinishAnimation;
264     }
265 
updateInternalState( float finalProgress, boolean isFinish, Runnable onFinishCallback)266     private void updateInternalState(
267             float finalProgress, boolean isFinish, Runnable onFinishCallback) {
268         if (mCurrentAnim != null) {
269             mCurrentAnim.cancel();
270             mCurrentAnim = null;
271         }
272 
273         boolean animateProgress =
274                 finalProgress >= mInternalStateProgress && getBounds().width() > 0;
275         if (!animateProgress || mRanFinishAnimation) {
276             setInternalProgress(finalProgress);
277             if (isFinish && onFinishCallback != null) {
278                 onFinishCallback.run();
279             }
280         } else {
281             mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
282             mCurrentAnim.setDuration(
283                     (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
284             mCurrentAnim.setInterpolator(LINEAR);
285             if (isFinish) {
286                 mCurrentAnim.addListener(new AnimatorListenerAdapter() {
287                     @Override
288                     public void onAnimationEnd(Animator animation) {
289                         mRanFinishAnimation = true;
290                     }
291                 });
292                 if (onFinishCallback != null) {
293                     mCurrentAnim.addListener(AnimatorListeners.forEndCallback(onFinishCallback));
294                 }
295             }
296             mCurrentAnim.start();
297         }
298     }
299 
300     @VisibleForTesting
getActiveAnimation()301     public ObjectAnimator getActiveAnimation() {
302         return mCurrentAnim;
303     }
304 
305     /**
306      * Sets the internal progress and updates the UI accordingly
307      *   for progress <= 0:
308      *     - icon is pending
309      *     - progress track is not visible
310      *     - progress bar is not visible
311      *   for progress < 1:
312      *     - icon without pending motion
313      *     - progress track is visible
314      *     - progress bar is visible. Progress bar is drawn as a fraction of
315      *       {@link #mScaledTrackPath}.
316      *       @see PathMeasure#getSegment(float, float, Path, boolean)
317      *   for progress > 1:
318      *     - scale the icon back to full size
319      */
setInternalProgress(float progress)320     private void setInternalProgress(float progress) {
321         // Animate scale and alpha from pending to downloading state.
322         if (progress > 0 && mInternalStateProgress == 0) {
323             // Progress is changing for the first time, animate the icon scale
324             Animator iconScaleAnimator = mIconScaleMultiplier.animateToValue(1);
325             iconScaleAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION);
326             iconScaleAnimator.setInterpolator(EMPHASIZED);
327             iconScaleAnimator.start();
328         }
329 
330         mInternalStateProgress = progress;
331         if (progress <= 0) {
332             mIconScaleMultiplier.updateValue(0);
333         } else {
334             mPathMeasure.getSegment(
335                     0, Math.min(progress, 1) * mTrackLength, mScaledProgressPath, true);
336             if (progress > 1) {
337                 // map the scale back to original value
338                 mIconScaleMultiplier.updateValue(Utilities.mapBoundToRange(
339                         progress - 1, 0, COMPLETE_ANIM_FRACTION, 1, 0, EMPHASIZED));
340             }
341         }
342         invalidateSelf();
343     }
344 
getPreloadColors(Context context)345     private static int[] getPreloadColors(Context context) {
346         int[] preloadColors = new int[2];
347 
348         preloadColors[PRELOAD_ACCENT_COLOR_INDEX] = Themes.getAttrColor(context,
349                 R.attr.preloadIconAccentColor);
350         preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX] = Themes.getAttrColor(context,
351                 R.attr.preloadIconBackgroundColor);
352 
353         return preloadColors;
354     }
355     /**
356      * Returns a FastBitmapDrawable with the icon.
357      */
newPendingIcon(Context context, ItemInfoWithIcon info)358     public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
359         return new PreloadIconDrawable(info, context);
360     }
361 
362     @Override
newConstantState()363     public FastBitmapConstantState newConstantState() {
364         return new PreloadIconConstantState(
365                 mBitmapInfo,
366                 mItem,
367                 mIndicatorColor,
368                 new int[] {mSystemAccentColor, mSystemBackgroundColor},
369                 mIsDarkMode,
370                 mShapePath);
371     }
372 
373     protected static class PreloadIconConstantState extends FastBitmapConstantState {
374 
375         protected final ItemInfoWithIcon mInfo;
376         protected final int mIndicatorColor;
377         protected final int[] mPreloadColors;
378         protected final boolean mIsDarkMode;
379         protected final int mLevel;
380         private final Path mShapePath;
381 
PreloadIconConstantState( BitmapInfo bitmapInfo, ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode, Path shapePath)382         public PreloadIconConstantState(
383                 BitmapInfo bitmapInfo,
384                 ItemInfoWithIcon info,
385                 int indicatorColor,
386                 int[] preloadColors,
387                 boolean isDarkMode,
388                 Path shapePath) {
389             super(bitmapInfo);
390             mInfo = info;
391             mIndicatorColor = indicatorColor;
392             mPreloadColors = preloadColors;
393             mIsDarkMode = isDarkMode;
394             mLevel = info.getProgressLevel();
395             mShapePath = shapePath;
396         }
397 
398         @Override
createDrawable()399         public PreloadIconDrawable createDrawable() {
400             return new PreloadIconDrawable(
401                     mInfo,
402                     mIndicatorColor,
403                     mPreloadColors,
404                     mIsDarkMode,
405                     mShapePath);
406         }
407     }
408 }
409