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