Files
Lawnchair/src/com/android/launcher3/graphics/PreloadIconDrawable.java
T
Federico Baron 4eb502ae10 Add finish icon scale animation for downloading apps
We add an animation when we finish downloading an app where the scale of the icon increases to from scale 0.867 to scale 1

Bug: 254858049
Test: Download an app and visualize finish download animation
Change-Id: I67912f86b0e35091a5fe1b39c3463a1ff203bd3b
2023-01-13 15:35:14 -08:00

474 lines
18 KiB
Java

/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.graphics;
import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
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.ColorFilter;
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.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;
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 {
private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
@Override
public Float get(PreloadIconDrawable object) {
return object.mInternalStateProgress;
}
@Override
public void set(PreloadIconDrawable object, Float value) {
object.setInternalProgress(value);
}
};
private static final int DEFAULT_PATH_SIZE = 100;
private static final int MAX_PAINT_ALPHA = 255;
private static final int TRACK_ALPHA = (int) (0.27f * MAX_PAINT_ALPHA);
private static final int DISABLED_ICON_ALPHA = (int) (0.6f * MAX_PAINT_ALPHA);
private static final long DURATION_SCALE = 500;
private static final long SCALE_AND_ALPHA_ANIM_DURATION = 500;
// The smaller the number, the faster the animation would be.
// Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE
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 = 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 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<>();
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();
private final ItemInfoWithIcon mItem;
// Path in [0, 100] bounds.
private final Path mShapePath;
private final Path mScaledTrackPath;
private final Path mScaledProgressPath;
private final Paint mProgressPaint;
private final int mIndicatorColor;
private final int mSystemAccentColor;
private final int mSystemBackgroundColor;
private final boolean mIsDarkMode;
private float mTrackLength;
private boolean mRanFinishAnimation;
private final 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;
// 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;
private boolean mIsStartable;
public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
this(
info,
IconPalette.getPreloadProgressColor(context, info.bitmap.color),
getPreloadColors(context),
Utilities.isDarkTheme(context),
getRefreshRateMillis(context));
}
public PreloadIconDrawable(
ItemInfoWithIcon info,
int indicatorColor,
int[] preloadColors,
boolean isDarkMode,
int refreshRateMillis) {
super(info.bitmap);
mItem = info;
mShapePath = GraphicsUtils.getShapePath(DEFAULT_PATH_SIZE);
mScaledTrackPath = new Path();
mScaledProgressPath = new Path();
mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
mIndicatorColor = indicatorColor;
mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX];
mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX];
mIsDarkMode = isDarkMode;
mRefreshRateMillis = refreshRateMillis;
// If it's a pending app we will animate scale and alpha when it's no longer pending.
mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1);
setLevel(info.getProgressLevel());
setIsStartable(info.isAppStartable());
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
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(PROGRESS_STROKE_SCALE * bounds.width());
mPathMeasure.setPath(mScaledTrackPath, true);
mTrackLength = mPathMeasure.getLength();
setInternalProgress(mInternalStateProgress);
}
@Override
public void drawInternal(Canvas canvas, Rect bounds) {
if (mRanFinishAnimation) {
super.drawInternal(canvas, bounds);
return;
}
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);
}
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();
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() && filter != null) {
reschedule();
}
}
@Override
protected void updateFilter() {
if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
setAlpha(mIsDisabled ? DISABLED_ICON_ALPHA : MAX_PAINT_ALPHA);
}
}
/**
* Updates the install progress based on the level
*/
@Override
protected boolean onLevelChange(int level) {
// Run the animation if we have already been bound.
updateInternalState(level * 0.01f, false, null);
return true;
}
/**
* Runs the finish animation if it is has not been run after last call to
* {@link #onLevelChange}
*/
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, onFinishCallback);
}
public boolean hasNotCompleted() {
return !mRanFinishAnimation;
}
/** Sets whether this icon should display the startable app UI. */
public void setIsStartable(boolean isStartable) {
if (mIsStartable != isStartable) {
mIsStartable = isStartable;
setIsDisabled(!isStartable);
}
}
private void updateInternalState(
float finalProgress, boolean isFinish, Runnable onFinishCallback) {
if (mCurrentAnim != null) {
mCurrentAnim.cancel();
mCurrentAnim = null;
}
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) {
mRanFinishAnimation = true;
}
});
}
mCurrentAnim.start();
}
}
/**
* Sets the internal progress and updates the UI accordingly
* for progress <= 0:
* - icon with pending motion
* - progress track is not visible
* - progress bar is not visible
* 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 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() && 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();
}
mInternalStateProgress = progress;
if (progress <= 0) {
if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
mScaledTrackPath.reset();
}
mIconScaleMultiplier.updateValue(0);
} else {
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();
}
private static int[] getPreloadColors(Context context) {
int[] preloadColors = new int[2];
preloadColors[PRELOAD_ACCENT_COLOR_INDEX] = Themes.getAttrColor(context,
R.attr.preloadIconAccentColor);
preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX] = Themes.getAttrColor(context,
R.attr.preloadIconBackgroundColor);
return preloadColors;
}
private static int getRefreshRateMillis(Context context) {
return RefreshRateTracker.getSingleFrameMs(context);
}
/**
* Returns a FastBitmapDrawable with the icon.
*/
public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
return new PreloadIconDrawable(info, context);
}
@Override
public FastBitmapConstantState newConstantState() {
return new PreloadIconConstantState(
mBitmap,
mIconColor,
mItem,
mIndicatorColor,
new int[] {mSystemAccentColor, mSystemBackgroundColor},
mIsDarkMode,
mRefreshRateMillis);
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
if (!visible) {
unscheduleSelf(mInvalidateRunnable);
}
return super.setVisible(visible, restart);
}
private void reschedule() {
unscheduleSelf(mInvalidateRunnable);
if (!isVisible()) {
return;
}
final long upTime = SystemClock.uptimeMillis();
scheduleSelf(mInvalidateRunnable,
upTime - ((upTime % mRefreshRateMillis)) + mRefreshRateMillis);
}
/**
* Returns a color filter to be used as an overlay on the pending icon with cascading motion
* based on its position.
*/
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();
int alpha = (int) Utilities.mapBoundToRange(
(int) ((time + waveMotionDelay) % ALPHA_DURATION_MILLIS),
0,
ALPHA_DURATION_MILLIS,
0,
OVERLAY_ALPHA_RANGE * 2,
LINEAR);
if (alpha > OVERLAY_ALPHA_RANGE) {
alpha = (OVERLAY_ALPHA_RANGE - (alpha % OVERLAY_ALPHA_RANGE));
}
int overlayColor = mIsDarkMode ? 0 : 255;
int currArgb = Color.argb(alpha, overlayColor, overlayColor, overlayColor);
return COLOR_FILTER_MAP.computeIfAbsent(currArgb, FILTER_FACTORY);
}
protected static class PreloadIconConstantState extends FastBitmapConstantState {
protected final ItemInfoWithIcon mInfo;
protected final int mIndicatorColor;
protected final int[] mPreloadColors;
protected final boolean mIsDarkMode;
protected final int mLevel;
protected final int mRefreshRateMillis;
public PreloadIconConstantState(
Bitmap bitmap,
int iconColor,
ItemInfoWithIcon info,
int indicatorColor,
int[] preloadColors,
boolean isDarkMode,
int refreshRateMillis) {
super(bitmap, iconColor);
mInfo = info;
mIndicatorColor = indicatorColor;
mPreloadColors = preloadColors;
mIsDarkMode = isDarkMode;
mLevel = info.getProgressLevel();
mRefreshRateMillis = refreshRateMillis;
}
@Override
public PreloadIconDrawable createDrawable() {
return new PreloadIconDrawable(
mInfo,
mIndicatorColor,
mPreloadColors,
mIsDarkMode,
mRefreshRateMillis);
}
}
}