Files
Lawnchair/src/com/android/launcher3/graphics/PreloadIconDrawable.java
T
Federico Baron 4ba9c11568 Improve color, contrast, and dimensions to match new spec for download icon
Here we update the values to the correct HCT values according to the specs, as well as the size of the icon

Bug: 267272201
Test: Turn on flag ENABLE_DOWNLOAD_APP_UX_V3 and download an app to see the changes.
Change-Id: I2cc2417f7500c4c08c41056bfeef3c33f4eab174
2023-04-13 23:53:20 +00:00

450 lines
16 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 static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V3;
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.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Rect;
import android.util.Property;
import androidx.core.graphics.ColorUtils;
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;
/**
* 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_V3.get() ? 0.8f : 0.7f;
private static final float PROGRESS_STROKE_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get()
? 0.06666667f
: 0.075f;
private static final float PROGRESS_BOUNDS_SCALE = 0.08f;
private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;
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 int mProgressColor;
private int mTrackColor;
private int mPlateColor;
private final boolean mIsDarkMode;
private float mTrackLength;
private boolean mRanFinishAnimation;
// 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),
GraphicsUtils.getShapePath(context, DEFAULT_PATH_SIZE));
}
public PreloadIconDrawable(
ItemInfoWithIcon info,
int indicatorColor,
int[] preloadColors,
boolean isDarkMode,
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);
if (ENABLE_DOWNLOAD_APP_UX_V3.get()) {
mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
}
mIndicatorColor = indicatorColor;
// This is the color
int primaryIconColor = mItem.bitmap.color;
// Progress color
float[] m3HCT = new float[3];
ColorUtils.colorToM3HCT(primaryIconColor, m3HCT);
mProgressColor = ColorUtils.M3HCTtoColor(
m3HCT[0],
m3HCT[1],
isDarkMode ? Math.max(m3HCT[2], 55) : Math.min(m3HCT[2], 40));
// Track color
mTrackColor = ColorUtils.M3HCTtoColor(
m3HCT[0],
16,
isDarkMode ? 30 : 90
);
// Plate color
mPlateColor = ColorUtils.M3HCTtoColor(
m3HCT[0],
isDarkMode ? 36 : 24,
isDarkMode ? (isThemed() ? 10 : 20) : 80
);
mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX];
mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX];
mIsDarkMode = isDarkMode;
// 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());
if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
setIsStartable(info.isAppStartable());
}
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
float progressWidth = bounds.width() * PROGRESS_BOUNDS_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 (mInternalStateProgress > 0
&& (ENABLE_DOWNLOAD_APP_UX_V3.get() || !ENABLE_DOWNLOAD_APP_UX_V2.get())) {
// Draw background.
mProgressPaint.setStyle(ENABLE_DOWNLOAD_APP_UX_V3.get()
? Paint.Style.FILL
: Paint.Style.FILL_AND_STROKE);
mProgressPaint.setColor(ENABLE_DOWNLOAD_APP_UX_V3.get()
? mPlateColor
: 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(ENABLE_DOWNLOAD_APP_UX_V3.get()
? mTrackColor
: mSystemAccentColor);
if (!ENABLE_DOWNLOAD_APP_UX_V3.get()) {
mProgressPaint.setAlpha(TRACK_ALPHA);
}
canvas.drawPath(mScaledTrackPath, mProgressPaint);
mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
if (ENABLE_DOWNLOAD_APP_UX_V3.get()) {
mProgressPaint.setColor(mProgressColor);
}
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());
super.drawInternal(canvas, bounds);
canvas.restoreToCount(saveCount);
}
@Override
protected void updateFilter() {
if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
setAlpha(mIsDisabled ? DISABLED_ICON_ALPHA : MAX_PAINT_ALPHA);
} else {
super.updateFilter();
}
}
/**
* 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) {
mProgressColor = oldIcon.mProgressColor;
mTrackColor = oldIcon.mTrackColor;
mPlateColor = oldIcon.mPlateColor;
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 is pending
* - 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;
}
/**
* 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,
mShapePath);
}
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;
private final Path mShapePath;
public PreloadIconConstantState(
Bitmap bitmap,
int iconColor,
ItemInfoWithIcon info,
int indicatorColor,
int[] preloadColors,
boolean isDarkMode,
Path shapePath) {
super(bitmap, iconColor);
mInfo = info;
mIndicatorColor = indicatorColor;
mPreloadColors = preloadColors;
mIsDarkMode = isDarkMode;
mLevel = info.getProgressLevel();
mShapePath = shapePath;
}
@Override
public PreloadIconDrawable createDrawable() {
return new PreloadIconDrawable(
mInfo,
mIndicatorColor,
mPreloadColors,
mIsDarkMode,
mShapePath);
}
}
}