799e12077b
Bug: 266661614 Test: Presubmit Change-Id: Ia3d0e0bc7a2b019d234e3bca71e893d5c4c54ca8
481 lines
18 KiB
Java
481 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 = 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),
|
|
GraphicsUtils.getShapePath(context, DEFAULT_PATH_SIZE));
|
|
}
|
|
|
|
public PreloadIconDrawable(
|
|
ItemInfoWithIcon info,
|
|
int indicatorColor,
|
|
int[] preloadColors,
|
|
boolean isDarkMode,
|
|
int refreshRateMillis,
|
|
Path shapePath) {
|
|
super(info.bitmap);
|
|
mItem = info;
|
|
mShapePath = shapePath;
|
|
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,
|
|
mShapePath);
|
|
}
|
|
|
|
@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;
|
|
private final Path mShapePath;
|
|
|
|
public PreloadIconConstantState(
|
|
Bitmap bitmap,
|
|
int iconColor,
|
|
ItemInfoWithIcon info,
|
|
int indicatorColor,
|
|
int[] preloadColors,
|
|
boolean isDarkMode,
|
|
int refreshRateMillis,
|
|
Path shapePath) {
|
|
super(bitmap, iconColor);
|
|
mInfo = info;
|
|
mIndicatorColor = indicatorColor;
|
|
mPreloadColors = preloadColors;
|
|
mIsDarkMode = isDarkMode;
|
|
mLevel = info.getProgressLevel();
|
|
mRefreshRateMillis = refreshRateMillis;
|
|
mShapePath = shapePath;
|
|
}
|
|
|
|
@Override
|
|
public PreloadIconDrawable createDrawable() {
|
|
return new PreloadIconDrawable(
|
|
mInfo,
|
|
mIndicatorColor,
|
|
mPreloadColors,
|
|
mIsDarkMode,
|
|
mRefreshRateMillis,
|
|
mShapePath);
|
|
}
|
|
}
|
|
}
|