From 5bf0742fc513696f2bcccfdebb11495b2603d788 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Fri, 27 Dec 2024 02:50:03 -0800 Subject: [PATCH] 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 --- .../HotseatPredictionController.java | 4 +- .../launcher3/taskbar/TaskbarView.java | 11 +- .../uioverrides/PredictedAppIcon.java | 226 ++++++++---------- src/com/android/launcher3/BubbleTextView.java | 18 -- 4 files changed, 110 insertions(+), 149 deletions(-) diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index c50e82dd91..c2cabd0557 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -229,9 +229,7 @@ public class HotseatPredictionController implements DragController.DragListener, (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 741853eb80..6b9f5a9d1e 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.ItemInfo; 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 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // 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 535ae1cb9e..caac35efe1 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.ArgbEvaluator; 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.Paint; 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 androidx.core.graphics.ColorUtils; 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.util.SafeCloseable; 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 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { 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 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 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { @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); - } - if (isSlotMachineAnimRunning) { - drawSlotMachineIcons(canvas); - } else { - super.onDraw(canvas); + 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()); + } } + 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 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { 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 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 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 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { @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 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { } 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 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { 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 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { 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 c78666ec6f..db5d7d45c7 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -369,27 +369,9 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @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) {