diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index f1f23c46fc..1ec5bb8ed2 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -166,5 +166,6 @@ 24dp 220dp 6dp - 28dp + 25dp + 4dp 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/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index da10bfb0fa..acabb0d9f7 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -34,6 +34,7 @@ import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.launcher3.util.OnboardingPrefs; @@ -45,6 +46,9 @@ import com.android.quickstep.SystemUiProxy; import com.android.quickstep.views.RecentsView; import com.android.systemui.shared.recents.model.ThumbnailData; +import java.util.Arrays; +import java.util.stream.Stream; + /** * A data source which integrates with a Launcher instance */ @@ -268,6 +272,11 @@ public class LauncherTaskbarUIController extends TaskbarUIController { mTaskbarOverrideBackgroundAlpha.updateValue(forceHide ? 0 : 1); } + @Override + public Stream getAppIconsForEdu() { + return Arrays.stream(mLauncher.getAppsView().getAppsStore().getApps()); + } + /** * Starts the taskbar education flow, if the user hasn't seen it yet. */ diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java index b32a41e987..11975430b1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java @@ -78,6 +78,7 @@ public class TaskbarControllers { taskbarKeyguardController.init(navbarButtonsViewController); stashedHandleViewController.init(this); taskbarStashController.init(this); + taskbarEduController.init(this); } /** diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java index ae9592d779..fd5c2ea6b2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java @@ -15,16 +15,72 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.anim.Interpolators.ACCEL_2; +import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; +import android.content.res.Resources; +import android.text.TextUtils; +import android.view.View; + import com.android.launcher3.R; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.uioverrides.PredictedAppIcon; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; /** Handles the Taskbar Education flow. */ public class TaskbarEduController { + private static final long WAVE_ANIM_DELAY = 250; + private static final long WAVE_ANIM_STAGGER = 50; + private static final long WAVE_ANIM_EACH_ICON_DURATION = 633; + private static final long WAVE_ANIM_SLOT_MACHINE_DURATION = 1085; + // The fraction of each icon's animation at which we reach the top point of the wave. + private static final float WAVE_ANIM_FRACTION_TOP = 0.4f; + // The fraction of each icon's animation at which we reach the bottom, before overshooting. + private static final float WAVE_ANIM_FRACTION_BOTTOM = 0.9f; + private static final TimeInterpolator WAVE_ANIM_TO_TOP_INTERPOLATOR = FAST_OUT_SLOW_IN; + private static final TimeInterpolator WAVE_ANIM_TO_BOTTOM_INTERPOLATOR = ACCEL_2; + private static final TimeInterpolator WAVE_ANIM_OVERSHOOT_INTERPOLATOR = DEACCEL; + private static final TimeInterpolator WAVE_ANIM_OVERSHOOT_RETURN_INTERPOLATOR = ACCEL_DEACCEL; + private static final float WAVE_ANIM_ICON_SCALE = 1.2f; + // How many icons to cycle through in the slot machine (+ the original icon at each end). + private static final int WAVE_ANIM_SLOT_MACHINE_NUM_ICONS = 3; + private final TaskbarActivityContext mActivity; + private final float mWaveAnimTranslationY; + private final float mWaveAnimTranslationYReturnOvershoot; + + // Initialized in init. + TaskbarControllers mControllers; + private TaskbarEduView mTaskbarEduView; + private Animator mAnim; public TaskbarEduController(TaskbarActivityContext activity) { mActivity = activity; + + final Resources resources = activity.getResources(); + mWaveAnimTranslationY = resources.getDimension(R.dimen.taskbar_edu_wave_anim_trans_y); + mWaveAnimTranslationYReturnOvershoot = resources.getDimension( + R.dimen.taskbar_edu_wave_anim_trans_y_return_overshoot); + } + + public void init(TaskbarControllers controllers) { + mControllers = controllers; } void showEdu() { @@ -35,6 +91,7 @@ public class TaskbarEduController { mTaskbarEduView.init(new TaskbarEduCallbacks()); mTaskbarEduView.addOnCloseListener(() -> mTaskbarEduView = null); mTaskbarEduView.show(); + startAnim(createWaveAnim()); }); } @@ -44,6 +101,90 @@ public class TaskbarEduController { } } + /** + * Starts the given animation, ending the previous animation first if it's still playing. + */ + private void startAnim(Animator anim) { + if (mAnim != null) { + mAnim.end(); + } + mAnim = anim; + mAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnim = null; + } + }); + mAnim.start(); + } + + /** + * Creates a staggered "wave" animation where each icon translates and scales up in succession. + */ + private Animator createWaveAnim() { + AnimatorSet waveAnim = new AnimatorSet(); + View[] icons = mControllers.taskbarViewController.getIconViews(); + for (int i = 0; i < icons.length; i++) { + View icon = icons[i]; + AnimatorSet iconAnim = new AnimatorSet(); + + Keyframe[] scaleKeyframes = new Keyframe[] { + Keyframe.ofFloat(0, 1f), + Keyframe.ofFloat(WAVE_ANIM_FRACTION_TOP, WAVE_ANIM_ICON_SCALE), + Keyframe.ofFloat(WAVE_ANIM_FRACTION_BOTTOM, 1f), + Keyframe.ofFloat(1f, 1f) + }; + scaleKeyframes[1].setInterpolator(WAVE_ANIM_TO_TOP_INTERPOLATOR); + scaleKeyframes[2].setInterpolator(WAVE_ANIM_TO_BOTTOM_INTERPOLATOR); + + Keyframe[] translationYKeyframes = new Keyframe[] { + Keyframe.ofFloat(0, 0f), + Keyframe.ofFloat(WAVE_ANIM_FRACTION_TOP, -mWaveAnimTranslationY), + Keyframe.ofFloat(WAVE_ANIM_FRACTION_BOTTOM, 0f), + // Half of the remaining fraction overshoots, then the other half returns to 0. + Keyframe.ofFloat( + WAVE_ANIM_FRACTION_BOTTOM + (1 - WAVE_ANIM_FRACTION_BOTTOM) / 2f, + mWaveAnimTranslationYReturnOvershoot), + Keyframe.ofFloat(1f, 0f) + }; + translationYKeyframes[1].setInterpolator(WAVE_ANIM_TO_TOP_INTERPOLATOR); + translationYKeyframes[2].setInterpolator(WAVE_ANIM_TO_BOTTOM_INTERPOLATOR); + translationYKeyframes[3].setInterpolator(WAVE_ANIM_OVERSHOOT_INTERPOLATOR); + translationYKeyframes[4].setInterpolator(WAVE_ANIM_OVERSHOOT_RETURN_INTERPOLATOR); + + iconAnim.play(ObjectAnimator.ofPropertyValuesHolder(icon, + PropertyValuesHolder.ofKeyframe(SCALE_PROPERTY, scaleKeyframes)) + .setDuration(WAVE_ANIM_EACH_ICON_DURATION)); + iconAnim.play(ObjectAnimator.ofPropertyValuesHolder(icon, + PropertyValuesHolder.ofKeyframe(View.TRANSLATION_Y, translationYKeyframes)) + .setDuration(WAVE_ANIM_EACH_ICON_DURATION)); + + if (icon instanceof PredictedAppIcon) { + // Play slot machine animation through random icons from AllAppsList. + PredictedAppIcon predictedAppIcon = (PredictedAppIcon) icon; + ItemInfo itemInfo = (ItemInfo) icon.getTag(); + List iconsToAnimate = mControllers.uiController.getAppIconsForEdu() + .filter(appInfo -> !TextUtils.equals(appInfo.title, itemInfo.title)) + .map(appInfo -> appInfo.bitmap) + .filter(bitmap -> !bitmap.isNullOrLowRes()) + .collect(Collectors.toList()); + // Pick n icons at random. + Collections.shuffle(iconsToAnimate); + if (iconsToAnimate.size() > WAVE_ANIM_SLOT_MACHINE_NUM_ICONS) { + iconsToAnimate = iconsToAnimate.subList(0, WAVE_ANIM_SLOT_MACHINE_NUM_ICONS); + } + Animator slotMachineAnim = predictedAppIcon.createSlotMachineAnim(iconsToAnimate); + if (slotMachineAnim != null) { + iconAnim.play(slotMachineAnim.setDuration(WAVE_ANIM_SLOT_MACHINE_DURATION)); + } + } + + iconAnim.setStartDelay(WAVE_ANIM_STAGGER * i); + waveAnim.play(iconAnim); + } + waveAnim.setStartDelay(WAVE_ANIM_DELAY); + return waveAnim; + } /** * Callbacks for {@link TaskbarEduView} to interact with its controller. diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduView.java index 9c4e844fa8..8525427a64 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduView.java @@ -15,7 +15,7 @@ */ package com.android.launcher3.taskbar; -import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE; import android.animation.PropertyValuesHolder; import android.content.Context; @@ -33,6 +33,7 @@ import com.android.launcher3.views.AbstractSlideInView; public class TaskbarEduView extends AbstractSlideInView implements Insettable { + private static final int DEFAULT_OPEN_DURATION = 500; private static final int DEFAULT_CLOSE_DURATION = 200; private final Rect mInsets = new Rect(); @@ -129,8 +130,8 @@ public class TaskbarEduView extends AbstractSlideInView mIsOpen = true; mOpenCloseAnimator.setValues( PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); - mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN); - mOpenCloseAnimator.start(); + mOpenCloseAnimator.setInterpolator(AGGRESSIVE_EASE); + mOpenCloseAnimator.setDuration(DEFAULT_OPEN_DURATION).start(); } void snapToPage(int page) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index df88e02d5e..c0312a01ac 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -17,6 +17,10 @@ package com.android.launcher3.taskbar; import android.graphics.Rect; +import com.android.launcher3.model.data.ItemInfoWithIcon; + +import java.util.stream.Stream; + /** * Base class for providing different taskbar UI */ @@ -35,4 +39,8 @@ public class TaskbarUIController { protected void updateContentInsets(Rect outContentInsets) { } protected void onStashedInAppChanged() { } + + public Stream getAppIconsForEdu() { + return Stream.empty(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 2280c491b5..5144d9a5f5 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -118,6 +118,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]; @@ -173,8 +174,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++; @@ -259,6 +266,18 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar return mIconLayoutBounds; } + /** + * Returns the app icons currently shown in the taskbar. + */ + public View[] getIconViews() { + final int count = getChildCount(); + View[] icons = new View[count]; + for (int i = 0; i < count; i++) { + icons[i] = getChildAt(i); + } + return icons; + } + // FolderIconParent implemented methods. @Override diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index a4b2e50fa8..40b0e18d27 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -121,6 +121,10 @@ public class TaskbarViewController { return mTaskbarView.getIconLayoutBounds(); } + public View[] getIconViews() { + return mTaskbarView.getIconViews(); + } + public AnimatedFloat getTaskbarIconScaleForStash() { return mTaskbarIconScaleForStash; } diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java index d839a36213..ee6e8cee9b 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java @@ -15,6 +15,16 @@ */ 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; import android.graphics.Canvas; @@ -23,8 +33,10 @@ import android.graphics.Matrix; 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.view.LayoutInflater; import android.view.ViewGroup; @@ -35,6 +47,8 @@ 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.AnimatorListeners; +import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.GraphicsUtils; import com.android.launcher3.icons.IconNormalizer; import com.android.launcher3.icons.LauncherIcons; @@ -45,6 +59,10 @@ 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 */ @@ -53,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); @@ -67,6 +88,25 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { private int mPlateColor; boolean mDrawForDrag = false; + // Used for the "slot-machine" education animation. + private List mSlotMachineIcons; + private Animator mSlotMachineAnim; + private float mSlotMachineIconTranslationY; + + private static final FloatProperty SLOT_MACHINE_TRANSLATION_Y = + new FloatProperty("slotMachineTranslationY") { + @Override + public void setValue(PredictedAppIcon predictedAppIcon, float transY) { + predictedAppIcon.mSlotMachineIconTranslationY = transY; + predictedAppIcon.invalidate(); + } + + @Override + public Float get(PredictedAppIcon predictedAppIcon) { + return predictedAppIcon.mSlotMachineIconTranslationY; + } + }; + public PredictedAppIcon(Context context) { this(context, null, 0); } @@ -88,15 +128,38 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { @Override public void onDraw(Canvas canvas) { int count = canvas.save(); + boolean isSlotMachineAnimRunning = mSlotMachineAnim != 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); } - super.onDraw(canvas); + if (isSlotMachineAnimRunning) { + drawSlotMachineIcons(canvas); + } else { + 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(); + } + @Override protected void drawDotIfNecessary(Canvas canvas) { mIsDrawingDot = true; @@ -109,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 { @@ -119,6 +190,76 @@ 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(); + } + } + + /** + * 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.newThemedIcon(mContext)) + .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(ACCEL_DEACCEL); + keyframes[2].setInterpolator(ACCEL_DEACCEL); + + mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this, + PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes)); + mSlotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> { + mSlotMachineIcons = null; + mSlotMachineAnim = null; + mSlotMachineIconTranslationY = 0; + invalidate(); + })); + return mSlotMachineAnim; } /** 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) {