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) {