From 175d152f76809a8b1e0a85395ddce687b437c9bf Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Wed, 25 Aug 2021 09:18:41 -0700 Subject: [PATCH] Animate PredictedAppIcon when its icon changes Reuses the slot machine animation to slide in the new icon. Additionally, staggers based on other icons changing before it. Test: open apps, watch predictions change Bug: 197780290 Change-Id: Ib2bc84193a9e350c915dd3486b6c98c6c88d3f83 --- .../HotseatPredictionController.java | 7 ++- .../launcher3/taskbar/TaskbarView.java | 11 +++- .../uioverrides/PredictedAppIcon.java | 57 +++++++++++++++++-- src/com/android/launcher3/BubbleTextView.java | 18 ++++++ 4 files changed, 86 insertions(+), 7 deletions(-) diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index b40a1d57a8..13baf5677a 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -200,6 +200,7 @@ public class HotseatPredictionController implements DragController.DragListener, } int predictionIndex = 0; + int numViewsAnimated = 0; ArrayList newItems = new ArrayList<>(); // make sure predicted icon removal and filling predictions don't step on each other if (mIconRemoveAnimators != null && mIconRemoveAnimators.isRunning()) { @@ -233,7 +234,11 @@ public class HotseatPredictionController implements DragController.DragListener, (WorkspaceItemInfo) mPredictedItems.get(predictionIndex++); if (isPredictedIcon(child) && child.isEnabled()) { PredictedAppIcon icon = (PredictedAppIcon) child; - icon.applyFromWorkspaceItem(predictedItem); + boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem); + icon.applyFromWorkspaceItem(predictedItem, animateIconChange, numViewsAnimated); + if (animateIconChange) { + numViewsAnimated++; + } icon.finishBinding(mPredictionLongClickListener); } else { newItems.add(predictedItem); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index f9767f3c2d..1a3b187db9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -115,6 +115,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar */ protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) { int nextViewIndex = 0; + int numViewsAnimated = 0; for (int i = 0; i < hotseatItemInfos.length; i++) { ItemInfo hotseatItemInfo = hotseatItemInfos[i]; @@ -170,8 +171,14 @@ 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 && hotseatItemInfo instanceof WorkspaceItemInfo) { - ((BubbleTextView) hotseatView).applyFromWorkspaceItem( - (WorkspaceItemInfo) hotseatItemInfo); + BubbleTextView btv = (BubbleTextView) hotseatView; + WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo; + + boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo); + btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated); + if (animate) { + numViewsAnimated++; + } } setClickAndLongClickListenersForIcon(hotseatView); nextViewIndex++; diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java index 265801da3c..ee6e8cee9b 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java @@ -18,9 +18,12 @@ package com.android.launcher3.uioverrides; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; import android.animation.Animator; +import android.animation.AnimatorSet; +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; @@ -57,6 +60,7 @@ import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.DoubleShadowBubbleTextView; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -67,6 +71,9 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { private static final int RING_SHADOW_COLOR = 0x99000000; private static final float RING_EFFECT_RATIO = 0.095f; + private static final long ICON_CHANGE_ANIM_DURATION = 360; + private static final long ICON_CHANGE_ANIM_STAGGER = 50; + boolean mIsDrawingDot = false; private final DeviceProfile mDeviceProfile; private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -165,9 +172,17 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { } @Override - public void applyFromWorkspaceItem(WorkspaceItemInfo info) { - super.applyFromWorkspaceItem(info); - mPlateColor = ColorUtils.setAlphaComponent(mDotParams.color, 200); + 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; + int newPlateColor = ColorUtils.setAlphaComponent(mDotParams.color, 200); + if (!animate) { + mPlateColor = newPlateColor; + } if (mIsPinned) { setContentDescription(info.contentDescription); } else { @@ -175,6 +190,22 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { getContext().getString(R.string.hotseat_prediction_content_description, info.contentDescription)); } + + 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) { + changeIconAnim.play(slotMachineAnim); + } + changeIconAnim.play(plateColorAnim); + changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER); + changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start(); + } } /** @@ -182,16 +213,34 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { * 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.newThemedIcon(mContext)) .forEach(mSlotMachineIcons::add); - mSlotMachineIcons.add(getIcon()); + if (endWithOriginalIcon) { + mSlotMachineIcons.add(getIcon()); + } float finalTrans = -getSlotMachineIconPlusSpacingSize() * (mSlotMachineIcons.size() - 1); Keyframe[] keyframes = new Keyframe[] { diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 353e52b176..02ec5e8a8b 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -256,9 +256,27 @@ 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, false); } + /** + * 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 LauncherAccessibilityDelegate) {