Fixing prediction ring color not getting updated if the icon changes
while the slot animation is running
Slot animation is only trigerred through prediction update and it sets
final ring color at that point as part of the animation. If the icon
and the color change due to some other reason (like high-res icon or
a package-update), that color is overriden by the ongoing slot animation.
Bug: 381897614
Test: Verified manually by slowing down animation
Flag: EXEMPT bugfix
Change-Id: Ia3b86330afdb91ba6ff3366e3c8057bd7ec34e5d
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index c50e82d..c2cabd0 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -229,9 +229,7 @@
(WorkspaceItemInfo) mPredictedItems.get(predictionIndex++);
if (isPredictedIcon(child) && child.isEnabled()) {
PredictedAppIcon icon = (PredictedAppIcon) child;
- boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem);
- icon.applyFromWorkspaceItem(predictedItem, animateIconChange, numViewsAnimated);
- if (animateIconChange) {
+ if (icon.applyFromWorkspaceItemWithAnimation(predictedItem, numViewsAnimated)) {
numViewsAnimated++;
}
icon.finishBinding(mPredictionLongClickListener);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 741853e..6b9f5a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -64,6 +64,7 @@
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer;
import com.android.launcher3.taskbar.customization.TaskbarDividerContainer;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.Themes;
@@ -595,10 +596,12 @@
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
if (hotseatView instanceof BubbleTextView btv
&& hotseatItemInfo instanceof WorkspaceItemInfo workspaceInfo) {
- boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
- btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
- if (animate) {
- numViewsAnimated++;
+ if (btv instanceof PredictedAppIcon pai) {
+ if (pai.applyFromWorkspaceItemWithAnimation(workspaceInfo, numViewsAnimated)) {
+ numViewsAnimated++;
+ }
+ } else {
+ btv.applyFromWorkspaceItem(workspaceInfo);
}
}
setClickAndLongClickListenersForIcon(hotseatView);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 535ae1c..caac35e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -16,7 +16,6 @@
package com.android.launcher3.uioverrides;
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
-import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
import android.animation.Animator;
@@ -26,8 +25,6 @@
import android.animation.Keyframe;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
-import android.annotation.Nullable;
import android.content.Context;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
@@ -37,7 +34,6 @@
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.os.Process;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Property;
@@ -48,12 +44,12 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.icons.LauncherIcons;
@@ -64,10 +60,6 @@
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
/**
* A BubbleTextView with a ring around it's drawable
*/
@@ -105,12 +97,12 @@
private final BlurMaskFilter mShadowFilter;
private boolean mIsPinned = false;
- private int mPlateColor;
+ private final AnimColorHolder mPlateColor = new AnimColorHolder();
boolean mDrawForDrag = false;
- // Used for the "slot-machine" education animation.
- private List<Drawable> mSlotMachineIcons;
- private Animator mSlotMachineAnim;
+ // Used for the "slot-machine" animation when prediction changes.
+ private final Rect mSlotIconBound = new Rect(0, 0, getIconSize(), getIconSize());
+ private Drawable mSlotMachineIcon;
private float mSlotMachineIconTranslationY;
// Used to animate the "ring" around predicted icons
@@ -153,34 +145,26 @@
@Override
public void onDraw(Canvas canvas) {
int count = canvas.save();
- boolean isSlotMachineAnimRunning = mSlotMachineAnim != null;
+ boolean isSlotMachineAnimRunning = mSlotMachineIcon != null;
if (!mIsPinned) {
drawEffect(canvas);
if (isSlotMachineAnimRunning) {
// Clip to to outside of the ring during the slot machine animation.
canvas.clipPath(mRingPath);
}
- canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
- canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
+ canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO,
+ getWidth() * .5f, getHeight() * .5f);
+ if (isSlotMachineAnimRunning) {
+ canvas.translate(0, mSlotMachineIconTranslationY);
+ mSlotMachineIcon.setBounds(mSlotIconBound);
+ mSlotMachineIcon.draw(canvas);
+ canvas.translate(0, getSlotMachineIconPlusSpacingSize());
+ }
}
- if (isSlotMachineAnimRunning) {
- drawSlotMachineIcons(canvas);
- } else {
- super.onDraw(canvas);
- }
+ super.onDraw(canvas);
canvas.restoreToCount(count);
}
- private void drawSlotMachineIcons(Canvas canvas) {
- canvas.translate((getWidth() - getIconSize()) / 2f,
- (getHeight() - getIconSize()) / 2f + mSlotMachineIconTranslationY);
- for (Drawable icon : mSlotMachineIcons) {
- icon.setBounds(0, 0, getIconSize(), getIconSize());
- icon.draw(canvas);
- canvas.translate(0, getSlotMachineIconPlusSpacingSize());
- }
- }
-
private float getSlotMachineIconPlusSpacingSize() {
return getIconSize() + getOutlineOffsetY();
}
@@ -196,104 +180,88 @@
mIsDrawingDot = false;
}
- @Override
- public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
- // Create the slot machine animation first, since it uses the current icon to start.
- Animator slotMachineAnim = animate
- ? createSlotMachineAnim(Collections.singletonList(info.bitmap), false)
- : null;
- super.applyFromWorkspaceItem(info, animate, staggerIndex);
- int oldPlateColor = mPlateColor;
+ /**
+ * Returns whether the newInfo differs from the current getTag().
+ */
+ private boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
+ boolean changedIcons = getTag() instanceof WorkspaceItemInfo oldInfo
+ && oldInfo.getTargetComponent() != null
+ && newInfo.getTargetComponent() != null
+ && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
+ return changedIcons && isShown();
+ }
- int newPlateColor;
+ @Override
+ public void applyIconAndLabel(ItemInfoWithIcon info) {
+ super.applyIconAndLabel(info);
if (getIcon().isThemed()) {
- newPlateColor = getResources().getColor(android.R.color.system_accent1_300);
+ mPlateColor.endColor = getResources().getColor(android.R.color.system_accent1_300);
} else {
float[] hctPlateColor = new float[3];
ColorUtils.colorToM3HCT(mDotParams.appColor, hctPlateColor);
- newPlateColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
+ mPlateColor.endColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
}
+ mPlateColor.onUpdate();
+ }
+
+ /**
+ * Tries to apply the icon with animation and returns true if the icon was indeed animated
+ */
+ public boolean applyFromWorkspaceItemWithAnimation(WorkspaceItemInfo info, int staggerIndex) {
+ boolean animate = shouldAnimateIconChange(info);
+ Drawable oldIcon = getIcon();
+ int oldPlateColor = mPlateColor.currentColor;
+ applyFromWorkspaceItem(info, null);
+
+ setContentDescription(
+ mIsPinned ? info.contentDescription :
+ getContext().getString(R.string.hotseat_prediction_content_description,
+ info.contentDescription));
if (!animate) {
- mPlateColor = newPlateColor;
- }
- if (mIsPinned) {
- setContentDescription(info.contentDescription);
+ mPlateColor.startColor = mPlateColor.endColor;
+ mPlateColor.progress.value = 1;
+ mPlateColor.onUpdate();
} else {
- setContentDescription(
- getContext().getString(R.string.hotseat_prediction_content_description,
- info.contentDescription));
- }
+ mPlateColor.startColor = oldPlateColor;
+ mPlateColor.progress.value = 0;
+ mPlateColor.onUpdate();
- if (animate) {
- ValueAnimator plateColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(),
- oldPlateColor, newPlateColor);
- plateColorAnim.addUpdateListener(valueAnimator -> {
- mPlateColor = (int) valueAnimator.getAnimatedValue();
- invalidate();
- });
AnimatorSet changeIconAnim = new AnimatorSet();
- if (slotMachineAnim != null) {
+
+ ObjectAnimator plateColorAnim =
+ ObjectAnimator.ofFloat(mPlateColor.progress, AnimatedFloat.VALUE, 0, 1);
+ plateColorAnim.setAutoCancel(true);
+ changeIconAnim.play(plateColorAnim);
+
+ if (!mIsPinned && oldIcon != null) {
+ // Play the slot machine icon
+ mSlotMachineIcon = oldIcon;
+
+ float finalTrans = -getSlotMachineIconPlusSpacingSize();
+ Keyframe[] keyframes = new Keyframe[] {
+ Keyframe.ofFloat(0f, 0f),
+ Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
+ Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
+ };
+ keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
+ keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
+
+ ObjectAnimator slotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
+ PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
+ slotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
+ mSlotMachineIcon = null;
+ mSlotMachineIconTranslationY = 0;
+ invalidate();
+ }));
+ slotMachineAnim.setAutoCancel(true);
changeIconAnim.play(slotMachineAnim);
}
- changeIconAnim.play(plateColorAnim);
+
changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER);
changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start();
}
- }
-
- /**
- * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning
- * and ending with the original icon.
- */
- public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate) {
- return createSlotMachineAnim(iconsToAnimate, true);
- }
-
- /**
- * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning
- * with the original icon, then cycling through the given icons, optionally ending back with
- * the original icon.
- * @param endWithOriginalIcon Whether we should land back on the icon we started with, rather
- * than the last item in iconsToAnimate.
- */
- public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate,
- boolean endWithOriginalIcon) {
- if (mIsPinned || iconsToAnimate == null || iconsToAnimate.isEmpty()) {
- return null;
- }
- if (mSlotMachineAnim != null) {
- mSlotMachineAnim.end();
- }
-
- // Bookend the other animating icons with the original icon on both ends.
- mSlotMachineIcons = new ArrayList<>(iconsToAnimate.size() + 2);
- mSlotMachineIcons.add(getIcon());
- iconsToAnimate.stream()
- .map(iconInfo -> iconInfo.newIcon(mContext, FLAG_THEMED))
- .forEach(mSlotMachineIcons::add);
- if (endWithOriginalIcon) {
- mSlotMachineIcons.add(getIcon());
- }
-
- float finalTrans = -getSlotMachineIconPlusSpacingSize() * (mSlotMachineIcons.size() - 1);
- Keyframe[] keyframes = new Keyframe[] {
- Keyframe.ofFloat(0f, 0f),
- Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
- Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
- };
- keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
- keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
-
- mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
- PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
- mSlotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
- mSlotMachineIcons = null;
- mSlotMachineAnim = null;
- mSlotMachineIconTranslationY = 0;
- invalidate();
- }));
- return mSlotMachineAnim;
+ return animate;
}
/**
@@ -345,6 +313,7 @@
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
+ mSlotIconBound.offsetTo((w - getIconSize()) / 2, (h - getIconSize()) / 2);
updateRingPath();
}
@@ -355,18 +324,12 @@
}
private void updateRingPath() {
- boolean isBadged = false;
- if (getTag() instanceof WorkspaceItemInfo) {
- WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
- isBadged = !Process.myUserHandle().equals(info.user)
- || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
- }
-
mRingPath.reset();
mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
-
mRingPath.addPath(mShapePath, mTmpMatrix);
- if (isBadged) {
+
+ FastBitmapDrawable icon = getIcon();
+ if (icon != null && icon.getBadge() != null) {
float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO;
float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
@@ -422,7 +385,7 @@
canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
}
canvas.drawPath(mRingPath, mIconRingPaint);
- mIconRingPaint.setColor(mPlateColor);
+ mIconRingPaint.setColor(mPlateColor.currentColor);
mIconRingPaint.setMaskFilter(null);
canvas.drawPath(mRingPath, mIconRingPaint);
canvas.restoreToCount(count);
@@ -474,6 +437,21 @@
return icon;
}
+ private class AnimColorHolder {
+
+ public final AnimatedFloat progress = new AnimatedFloat(this::onUpdate, 1);
+ public final ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
+ public Integer startColor = 0;
+ public Integer endColor = 0;
+
+ public int currentColor = 0;
+
+ private void onUpdate() {
+ currentColor = (Integer) evaluator.evaluate(progress.value, startColor, endColor);
+ invalidate();
+ }
+ }
+
/**
* Draws Predicted Icon outline on cell layout
*/
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index c78666e..db5d7d4 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -369,27 +369,9 @@
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
- applyFromWorkspaceItem(info, /* animate = */ false, /* staggerIndex = */ 0);
- }
-
- @UiThread
- public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
applyFromWorkspaceItem(info, null);
}
- /**
- * Returns whether the newInfo differs from the current getTag().
- */
- public boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
- WorkspaceItemInfo oldInfo = getTag() instanceof WorkspaceItemInfo
- ? (WorkspaceItemInfo) getTag()
- : null;
- boolean changedIcons = oldInfo != null && oldInfo.getTargetComponent() != null
- && newInfo.getTargetComponent() != null
- && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
- return changedIcons && isShown();
- }
-
@Override
public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
if (delegate instanceof BaseAccessibilityDelegate) {