Merge "Add finish icon scale animation for downloading apps" into tm-qpr-dev am: 196569bb50

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/20732560

Change-Id: Ia4e3489346f6a9a6220ed91d5894cd986fcf7267
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
TreeHugger Robot
2023-01-17 20:01:13 +00:00
committed by Automerger Merge Worker
3 changed files with 121 additions and 139 deletions
+18 -15
View File
@@ -16,11 +16,14 @@
package com.android.launcher3;
import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -291,7 +294,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
applyFromWorkspaceItem(info, false);
applyFromWorkspaceItem(info, null);
}
/**
@@ -320,10 +323,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
}
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
public void applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon) {
applyIconAndLabel(info);
setItemInfo(info);
applyLoadingState(promiseStateChanged);
applyLoadingState(icon);
applyDotState(info, false /* animate */);
setDownloadStateContentDescription(info, info.getProgressLevel());
}
@@ -710,23 +713,23 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
* If this app is installed and downloading incrementally, the progress bar will be updated
* with the total download progress.
*/
public void applyLoadingState(boolean promiseStateChanged) {
public void applyLoadingState(PreloadIconDrawable icon) {
if (getTag() instanceof ItemInfoWithIcon) {
WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE)
!= 0) {
updateProgressBarUi(info.getProgressLevel() == 100);
} else if (info.hasPromiseIconUi() || (info.runtimeStatusFlags
& ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
updateProgressBarUi(promiseStateChanged);
if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0
|| info.hasPromiseIconUi()
|| (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0
|| (ENABLE_DOWNLOAD_APP_UX_V2.get() && icon != null)) {
updateProgressBarUi(icon);
}
}
}
private void updateProgressBarUi(boolean maybePerformFinishedAnimation) {
private void updateProgressBarUi(PreloadIconDrawable oldIcon) {
FastBitmapDrawable originalIcon = mIcon;
PreloadIconDrawable preloadDrawable = applyProgressLevel();
if (preloadDrawable != null && maybePerformFinishedAnimation) {
preloadDrawable.maybePerformFinishedAnimation();
if (preloadDrawable != null && oldIcon != null) {
preloadDrawable.maybePerformFinishedAnimation(oldIcon, () -> setIcon(originalIcon));
}
}
@@ -824,12 +827,12 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
!= 0) {
String percentageString = NumberFormat.getPercentInstance()
.format(progressLevel * 0.01);
if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
setContentDescription(getContext()
.getString(
R.string.app_installing_title, info.title, percentageString));
} else if ((info.runtimeStatusFlags
& ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
& FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
setContentDescription(getContext()
.getString(
R.string.app_downloading_title, info.title, percentageString));
@@ -28,6 +28,7 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
@@ -41,6 +42,7 @@ import android.util.Property;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -53,7 +55,7 @@ import java.util.function.Function;
/**
* Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
*/
public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable {
public class PreloadIconDrawable extends FastBitmapDrawable {
private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
@@ -78,16 +80,19 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
// The smaller the number, the faster the animation would be.
// Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
private static final float COMPLETE_ANIM_FRACTION = 0.3f;
private static final float COMPLETE_ANIM_FRACTION = 1f;
private static final float SMALL_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get() ? 0.867f : 0.7f;
private static final float PROGRESS_STROKE_SCALE = 0.075f;
private static final float PROGRESS_STROKE_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get()
? 0.0655f
: 0.075f;
private static final float PROGRESS_BOUNDS_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 int OVERLAY_ALPHA_RANGE = 127;
private static final long WAVE_MOTION_DELAY_FACTOR_MILLIS = 100;
private static final WeakHashMap<Integer, PorterDuffColorFilter> COLOR_FILTER_MAP =
new WeakHashMap<>();
@@ -111,19 +116,17 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
private final int mSystemBackgroundColor;
private final boolean mIsDarkMode;
private int mTrackAlpha;
private float mTrackLength;
private boolean mRanFinishAnimation;
private final int mRefreshRateMillis;
private final AnimatedFloat mIconScale = new AnimatedFloat(this::invalidateSelf);
private final AnimatedFloat mOverlayAlpha = new AnimatedFloat(this::updateOverlayAlpha);
private boolean mShouldAnimateScaleAndAlpha;
// 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;
// This multiplier is used to animate scale when going from 0 to non-zero and expanding
private final Runnable mInvalidateRunnable = this::invalidateSelf;
private final AnimatedFloat mIconScaleMultiplier = new AnimatedFloat(mInvalidateRunnable);
private ObjectAnimator mCurrentAnim;
@@ -160,10 +163,7 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
mRefreshRateMillis = refreshRateMillis;
// If it's a pending app we will animate scale and alpha when it's no longer pending.
if (ENABLE_DOWNLOAD_APP_UX_V2.get() && info.getProgressLevel() == 0) {
mShouldAnimateScaleAndAlpha = true;
mOverlayAlpha.updateValue(127);
}
mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1);
setLevel(info.getProgressLevel());
setIsStartable(info.isAppStartable());
@@ -173,14 +173,17 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
float progressWidth = PROGRESS_STROKE_SCALE * bounds.width();
float progressWidth = bounds.width() * (ENABLE_DOWNLOAD_APP_UX_V2.get()
? PROGRESS_BOUNDS_SCALE
: PROGRESS_STROKE_SCALE);
mTmpMatrix.setScale(
(bounds.width() - 2 * progressWidth) / DEFAULT_PATH_SIZE,
(bounds.height() - 2 * progressWidth) / DEFAULT_PATH_SIZE);
mTmpMatrix.postTranslate(bounds.left + progressWidth, bounds.top + progressWidth);
mShapePath.transform(mTmpMatrix, mScaledTrackPath);
mProgressPaint.setStrokeWidth(progressWidth);
mProgressPaint.setStrokeWidth(PROGRESS_STROKE_SCALE * bounds.width());
mPathMeasure.setPath(mScaledTrackPath, true);
mTrackLength = mPathMeasure.getLength();
@@ -195,26 +198,35 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
return;
}
// Draw background.
mProgressPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mProgressPaint.setColor(mSystemBackgroundColor);
canvas.drawPath(mScaledTrackPath, mProgressPaint);
if (!ENABLE_DOWNLOAD_APP_UX_V2.get() && mInternalStateProgress > 0) {
// Draw background.
mProgressPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mProgressPaint.setColor(mSystemBackgroundColor);
canvas.drawPath(mScaledTrackPath, mProgressPaint);
}
// Draw track and progress.
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setColor(mIsStartable ? mIndicatorColor : mSystemAccentColor);
mProgressPaint.setAlpha(TRACK_ALPHA);
canvas.drawPath(mScaledTrackPath, mProgressPaint);
mProgressPaint.setAlpha(mTrackAlpha);
canvas.drawPath(mScaledProgressPath, mProgressPaint);
if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
// Draw track and progress.
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setColor(mSystemAccentColor);
mProgressPaint.setAlpha(TRACK_ALPHA);
canvas.drawPath(mScaledTrackPath, mProgressPaint);
mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
canvas.drawPath(mScaledProgressPath, mProgressPaint);
}
int saveCount = canvas.save();
canvas.scale(
mIconScale.value, mIconScale.value, bounds.exactCenterX(), bounds.exactCenterY());
float scale = ENABLE_DOWNLOAD_APP_UX_V2.get()
? 1 - mIconScaleMultiplier.value * (1 - SMALL_SCALE)
: SMALL_SCALE;
canvas.scale(scale, scale, bounds.exactCenterX(), bounds.exactCenterY());
ColorFilter filter = getOverlayFilter();
mPaint.setColorFilter(filter);
super.drawInternal(canvas, bounds);
canvas.restoreToCount(saveCount);
if (ENABLE_DOWNLOAD_APP_UX_V2.get() && mInternalStateProgress == 0) {
if (ENABLE_DOWNLOAD_APP_UX_V2.get() && filter != null) {
reschedule();
}
}
@@ -232,7 +244,7 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
@Override
protected boolean onLevelChange(int level) {
// Run the animation if we have already been bound.
updateInternalState(level * 0.01f, getBounds().width() > 0, false);
updateInternalState(level * 0.01f, false, null);
return true;
}
@@ -240,12 +252,18 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
* Runs the finish animation if it is has not been run after last call to
* {@link #onLevelChange}
*/
public void maybePerformFinishedAnimation() {
public void maybePerformFinishedAnimation(
PreloadIconDrawable oldIcon, Runnable onFinishCallback) {
if (oldIcon.mInternalStateProgress >= 1) {
mInternalStateProgress = oldIcon.mInternalStateProgress;
}
// If the drawable was recently initialized, skip the progress animation.
if (mInternalStateProgress == 0) {
mInternalStateProgress = 1;
}
updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true);
updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, onFinishCallback);
}
public boolean hasNotCompleted() {
@@ -260,26 +278,29 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
}
}
private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
private void updateInternalState(
float finalProgress, boolean isFinish, Runnable onFinishCallback) {
if (mCurrentAnim != null) {
mCurrentAnim.cancel();
mCurrentAnim = null;
}
if (Float.compare(finalProgress, mInternalStateProgress) == 0) {
return;
}
if (finalProgress < mInternalStateProgress) {
shouldAnimate = false;
}
if (!shouldAnimate || mRanFinishAnimation) {
boolean animateProgress =
finalProgress >= mInternalStateProgress && getBounds().width() > 0;
if (!animateProgress || mRanFinishAnimation) {
setInternalProgress(finalProgress);
if (isFinish && onFinishCallback != null) {
onFinishCallback.run();
}
} else {
mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
mCurrentAnim.setDuration(
(long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
mCurrentAnim.setInterpolator(LINEAR);
if (isFinish) {
if (onFinishCallback != null) {
mCurrentAnim.addListener(AnimatorListeners.forEndCallback(onFinishCallback));
}
mCurrentAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -297,62 +318,38 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
* - icon with pending motion
* - progress track is not visible
* - progress bar is not visible
* for progress < 1
* for progress < 1:
* - icon without pending motion
* - progress track is visible
* - 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)
* - we calculate fraction of progress in the above range
* - progress track is drawn with alpha based on fraction
* - progress bar is drawn at 100% with alpha based on fraction
* - icon is scaled up based on fraction and is drawn in enabled state
* for progress >= (1 + COMPLETE_ANIM_FRACTION)
* - only icon is drawn in normal state
* for progress > 1:
* - scale the icon back to full size
*/
private void setInternalProgress(float progress) {
// Animate scale and alpha from pending to downloading state.
if (ENABLE_DOWNLOAD_APP_UX_V2.get()
&& mShouldAnimateScaleAndAlpha && mInternalStateProgress == 0 && progress > 0) {
Animator iconScaleAnimator = mIconScale.animateToValue(SMALL_SCALE);
if (ENABLE_DOWNLOAD_APP_UX_V2.get() && progress > 0 && mInternalStateProgress == 0) {
// Progress is changing for the first time, animate the icon scale
Animator iconScaleAnimator = mIconScaleMultiplier.animateToValue(1);
iconScaleAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION);
iconScaleAnimator.setInterpolator(EMPHASIZED);
iconScaleAnimator.start();
Animator overlayAlphaAnimator = mOverlayAlpha.animateToValue(0);
overlayAlphaAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION);
overlayAlphaAnimator.setInterpolator(EMPHASIZED);
overlayAlphaAnimator.start();
}
mInternalStateProgress = progress;
if (progress <= 0) {
mIconScale.updateValue(ENABLE_DOWNLOAD_APP_UX_V2.get() ? 1 : SMALL_SCALE);
mScaledTrackPath.reset();
mTrackAlpha = MAX_PAINT_ALPHA;
} else if (progress < 1) {
mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
if (ENABLE_DOWNLOAD_APP_UX_V2.get()) {
mPathMeasure.getSegment(0, mTrackLength, mScaledTrackPath, true);
if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
mScaledTrackPath.reset();
}
if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || !mShouldAnimateScaleAndAlpha) {
mIconScale.updateValue(SMALL_SCALE);
}
mTrackAlpha = MAX_PAINT_ALPHA;
mIconScaleMultiplier.updateValue(0);
} else {
setIsDisabled(mItem.isDisabled());
mScaledTrackPath.set(mScaledProgressPath);
float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION;
if (fraction >= 1) {
// Animation has completed
mIconScale.updateValue(1);
mTrackAlpha = 0;
} else {
mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA);
mIconScale.updateValue(SMALL_SCALE + (1 - SMALL_SCALE) * fraction);
mPathMeasure.getSegment(
0, Math.min(progress, 1) * mTrackLength, mScaledProgressPath, true);
if (progress > 1 && ENABLE_DOWNLOAD_APP_UX_V2.get()) {
// map the scale back to original value
mIconScaleMultiplier.updateValue(Utilities.mapBoundToRange(
progress - 1, 0, COMPLETE_ANIM_FRACTION, 1, 0, EMPHASIZED));
}
}
invalidateSelf();
@@ -392,72 +389,49 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
mRefreshRateMillis);
}
@Override
public void run() {
if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
return;
}
if (!applyPendingIconOverlay()) {
reschedule();
}
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean result = super.setVisible(visible, restart);
if (visible) {
reschedule();
} else {
unscheduleSelf(this);
if (!visible) {
unscheduleSelf(mInvalidateRunnable);
}
return result;
return super.setVisible(visible, restart);
}
private void reschedule() {
unscheduleSelf(this);
unscheduleSelf(mInvalidateRunnable);
if (!isVisible()) {
return;
}
final long upTime = SystemClock.uptimeMillis();
scheduleSelf(this, upTime - ((upTime % mRefreshRateMillis)) + mRefreshRateMillis);
scheduleSelf(mInvalidateRunnable,
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.
* Returns a color filter to be used as an overlay on the pending icon with cascading motion
* based on its position.
*/
private boolean applyPendingIconOverlay() {
private ColorFilter getOverlayFilter() {
if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
// If the download has started, we do no need to animate
return null;
}
long waveMotionDelay = (mItem.cellX * WAVE_MOTION_DELAY_FACTOR_MILLIS)
+ (mItem.cellY * WAVE_MOTION_DELAY_FACTOR_MILLIS);
long time = SystemClock.uptimeMillis();
float newAlpha = Utilities.mapBoundToRange(
(float) (time + waveMotionDelay) % ALPHA_DURATION_MILLIS,
int alpha = (int) Utilities.mapBoundToRange(
(int) ((time + waveMotionDelay) % ALPHA_DURATION_MILLIS),
0,
ALPHA_DURATION_MILLIS,
0,
MAX_PAINT_ALPHA,
OVERLAY_ALPHA_RANGE * 2,
LINEAR);
if (newAlpha > OVERLAY_ALPHA_RANGE) {
newAlpha = (OVERLAY_ALPHA_RANGE - (newAlpha % OVERLAY_ALPHA_RANGE));
if (alpha > OVERLAY_ALPHA_RANGE) {
alpha = (OVERLAY_ALPHA_RANGE - (alpha % OVERLAY_ALPHA_RANGE));
}
boolean invalidate = false;
if ((int) mOverlayAlpha.value != newAlpha) {
mOverlayAlpha.updateValue(newAlpha);
invalidate = true;
}
return invalidate;
}
private void updateOverlayAlpha() {
int overlayColor = mIsDarkMode ? 0 : 255;
int currArgb =
Color.argb((int) mOverlayAlpha.value, overlayColor, overlayColor, overlayColor);
mPaint.setColorFilter(COLOR_FILTER_MAP.computeIfAbsent(currArgb, FILTER_FACTORY));
invalidateSelf();
int currArgb = Color.argb(alpha, overlayColor, overlayColor, overlayColor);
return COLOR_FILTER_MAP.computeIfAbsent(currArgb, FILTER_FACTORY);
}
protected static class PreloadIconConstantState extends FastBitmapConstantState {
@@ -50,7 +50,12 @@ public interface LauncherBindableItemsContainer {
Drawable oldIcon = shortcut.getIcon();
boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
&& ((PreloadIconDrawable) oldIcon).hasNotCompleted();
shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
shortcut.applyFromWorkspaceItem(
si,
si.isPromise() != oldPromiseState
&& oldIcon instanceof PreloadIconDrawable
? (PreloadIconDrawable) oldIcon
: null);
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
((FolderIcon) v).updatePreviewItems(updates::contains);
}
@@ -74,7 +79,7 @@ public interface LauncherBindableItemsContainer {
ItemOperator op = (info, v) -> {
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
&& updates.contains(info)) {
((BubbleTextView) v).applyLoadingState(false /* promiseStateChanged */);
((BubbleTextView) v).applyLoadingState(null);
} else if (v instanceof PendingAppWidgetHostView
&& info instanceof LauncherAppWidgetInfo
&& updates.contains(info)) {