Improve restore icon treatment
This is the first of a few CL's that change the way pending/downloading/installing apps look to follow the new mocks. In this CL we:
-Fix a regression where the track path doesn't show up when downloading an app
-Change the sizes of icons to reflect the new design mock
-Add skeleton motion animation
In upcoming CL's we will add visual polish and make sure it matches the motion spec exactly
Bug: 254858049
Test: Download an app and check pending, downloading, and installing visuals
Change-Id: Ief1038ea52d77874b4031db76d51a2787615a14d
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index 0eb86b1..9a961ca 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -17,31 +17,41 @@
package com.android.launcher3.graphics;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
+import android.os.SystemClock;
import android.util.Property;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.window.RefreshRateTracker;
+
+import java.util.WeakHashMap;
+import java.util.function.Function;
/**
* Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
*/
-public class PreloadIconDrawable extends FastBitmapDrawable {
+public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable {
private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
@@ -67,12 +77,20 @@
// Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
private static final float COMPLETE_ANIM_FRACTION = 0.3f;
- private static final float SMALL_SCALE = 0.7f;
+ private static final float SMALL_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get() ? 0.85f : 0.7f;
private static final float PROGRESS_STROKE_SCALE = 0.075f;
private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;
+ private static final int ALPHA_DURATION_MILLIS = 3000;
+ private static final float OVERLAY_ALPHA_RANGE = 127.5f;
+ private static final long WAVE_MOTION_DELAY_FACTOR_MILLIS = 100;
+ private static final WeakHashMap<Integer, PorterDuffColorFilter> COLOR_FILTER_MAP =
+ new WeakHashMap<>();
+ public static final Function<Integer, PorterDuffColorFilter> FILTER_FACTORY =
+ currArgb -> new PorterDuffColorFilter(currArgb, PorterDuff.Mode.SRC_ATOP);
+
private final Matrix mTmpMatrix = new Matrix();
private final PathMeasure mPathMeasure = new PathMeasure();
@@ -96,6 +114,9 @@
private boolean mRanFinishAnimation;
+ private int mOverlayAlpha = 127;
+ private int mRefreshRateMillis;
+
// Progress of the internal state. [0, 1] indicates the fraction of completed progress,
// [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation.
private float mInternalStateProgress;
@@ -109,14 +130,16 @@
info,
IconPalette.getPreloadProgressColor(context, info.bitmap.color),
getPreloadColors(context),
- Utilities.isDarkTheme(context));
+ Utilities.isDarkTheme(context),
+ getRefreshRateMillis(context));
}
public PreloadIconDrawable(
ItemInfoWithIcon info,
int indicatorColor,
int[] preloadColors,
- boolean isDarkMode) {
+ boolean isDarkMode,
+ int refreshRateMillis) {
super(info.bitmap);
mItem = info;
mShapePath = GraphicsUtils.getShapePath(DEFAULT_PATH_SIZE);
@@ -130,6 +153,7 @@
mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX];
mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX];
mIsDarkMode = isDarkMode;
+ mRefreshRateMillis = refreshRateMillis;
setLevel(info.getProgressLevel());
setIsStartable(info.isAppStartable());
@@ -178,11 +202,17 @@
canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY());
super.drawInternal(canvas, bounds);
canvas.restoreToCount(saveCount);
+
+ if (ENABLE_DOWNLOAD_APP_UX_V2.get() && mInternalStateProgress == 0) {
+ reschedule();
+ }
}
@Override
protected void updateFilter() {
- setAlpha(mIsDisabled ? DISABLED_ICON_ALPHA : MAX_PAINT_ALPHA);
+ if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
+ setAlpha(mIsDisabled ? DISABLED_ICON_ALPHA : MAX_PAINT_ALPHA);
+ }
}
/**
@@ -237,7 +267,7 @@
mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
mCurrentAnim.setDuration(
(long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
- mCurrentAnim.setInterpolator(Interpolators.LINEAR);
+ mCurrentAnim.setInterpolator(LINEAR);
if (isFinish) {
mCurrentAnim.addListener(new AnimatorListenerAdapter() {
@Override
@@ -253,13 +283,13 @@
/**
* Sets the internal progress and updates the UI accordingly
* for progress <= 0:
- * - icon in the small scale and disabled state
- * - progress track is visible
+ * - icon with pending motion
+ * - progress track is not visible
* - progress bar is not visible
- * for 0 < progress < 1
- * - icon in the small scale and disabled state
+ * for progress < 1
+ * - icon without pending motion
* - progress track is visible
- * - progress bar is visible with dominant color. Progress bar is drawn as a fraction of
+ * - progress bar is visible. Progress bar is drawn as a fraction of
* {@link #mScaledTrackPath}.
* @see PathMeasure#getSegment(float, float, Path, boolean)
* for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION)
@@ -273,16 +303,18 @@
private void setInternalProgress(float progress) {
mInternalStateProgress = progress;
if (progress <= 0) {
- mIconScale = SMALL_SCALE;
+ mIconScale = ENABLE_DOWNLOAD_APP_UX_V2.get() ? 1 : SMALL_SCALE;
mScaledTrackPath.reset();
mTrackAlpha = MAX_PAINT_ALPHA;
- }
-
- if (progress < 1 && progress > 0) {
+ } else if (progress < 1) {
mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
+ if (ENABLE_DOWNLOAD_APP_UX_V2.get()) {
+ mPaint.setColorFilter(null);
+ mPathMeasure.getSegment(0, mTrackLength, mScaledTrackPath, true);
+ }
mIconScale = SMALL_SCALE;
mTrackAlpha = MAX_PAINT_ALPHA;
- } else if (progress >= 1) {
+ } else {
setIsDisabled(mItem.isDisabled());
mScaledTrackPath.set(mScaledProgressPath);
float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION;
@@ -310,6 +342,10 @@
return preloadColors;
}
+ private static int getRefreshRateMillis(Context context) {
+ return RefreshRateTracker.getSingleFrameMs(context);
+ }
+
/**
* Returns a FastBitmapDrawable with the icon.
*/
@@ -325,7 +361,75 @@
mItem,
mIndicatorColor,
new int[] {mSystemAccentColor, mSystemBackgroundColor},
- mIsDarkMode);
+ mIsDarkMode,
+ mRefreshRateMillis);
+ }
+
+ @Override
+ public void run() {
+ if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
+ return;
+ }
+ if (applyPendingIconOverlay()) {
+ invalidateSelf();
+ } else {
+ reschedule();
+ }
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ boolean result = super.setVisible(visible, restart);
+ if (visible) {
+ reschedule();
+ } else {
+ unscheduleSelf(this);
+ }
+ return result;
+ }
+
+ private void reschedule() {
+ unscheduleSelf(this);
+
+ if (!isVisible()) {
+ return;
+ }
+
+ final long upTime = SystemClock.uptimeMillis();
+ scheduleSelf(this, upTime - ((upTime % mRefreshRateMillis)) + mRefreshRateMillis);
+ }
+
+
+ /**
+ * Apply an overlay on the pending icon with cascading motion based on its position.
+ * Returns {@code true} if the icon alpha is updated, so that we re-draw.
+ */
+ private boolean applyPendingIconOverlay() {
+ long waveMotionDelay = (mItem.cellX * WAVE_MOTION_DELAY_FACTOR_MILLIS)
+ + (mItem.cellY * WAVE_MOTION_DELAY_FACTOR_MILLIS);
+ long time = SystemClock.uptimeMillis();
+ int newAlpha = (int) Utilities.mapBoundToRange(
+ (float) (time + waveMotionDelay) % ALPHA_DURATION_MILLIS,
+ 0,
+ ALPHA_DURATION_MILLIS,
+ 0,
+ MAX_PAINT_ALPHA,
+ LINEAR);
+ if (newAlpha > OVERLAY_ALPHA_RANGE) {
+ newAlpha = (int) (OVERLAY_ALPHA_RANGE - (newAlpha % OVERLAY_ALPHA_RANGE));
+ }
+
+ boolean invalidate = false;
+ if (mOverlayAlpha != newAlpha) {
+ mOverlayAlpha = newAlpha;
+ int overlayColor = mIsDarkMode ? 0 : 255;
+ int currArgb = Color.argb(mOverlayAlpha, overlayColor, overlayColor, overlayColor);
+ mPaint.setColorFilter(COLOR_FILTER_MAP.computeIfAbsent(
+ currArgb,
+ FILTER_FACTORY));
+ invalidate = true;
+ }
+ return invalidate;
}
protected static class PreloadIconConstantState extends FastBitmapConstantState {
@@ -335,6 +439,7 @@
protected final int[] mPreloadColors;
protected final boolean mIsDarkMode;
protected final int mLevel;
+ protected final int mRefreshRateMillis;
public PreloadIconConstantState(
Bitmap bitmap,
@@ -342,13 +447,15 @@
ItemInfoWithIcon info,
int indicatorColor,
int[] preloadColors,
- boolean isDarkMode) {
+ boolean isDarkMode,
+ int refreshRateMillis) {
super(bitmap, iconColor);
mInfo = info;
mIndicatorColor = indicatorColor;
mPreloadColors = preloadColors;
mIsDarkMode = isDarkMode;
mLevel = info.getProgressLevel();
+ mRefreshRateMillis = refreshRateMillis;
}
@Override
@@ -357,7 +464,8 @@
mInfo,
mIndicatorColor,
mPreloadColors,
- mIsDarkMode);
+ mIsDarkMode,
+ mRefreshRateMillis);
}
}
}