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