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