From 98dea57d23cf374f8a1ace03c7678248dda85b5a Mon Sep 17 00:00:00 2001 From: Michael Mikhail Date: Thu, 2 Feb 2023 23:11:53 +0000 Subject: [PATCH] [Media TTT] Add bounce animation to the receiver icon Add a bounce animation to app icon and the ripple behinds it using value animator. It repeats a vertical translation of view until the animator is canceled when the view animates out. Bug: 254265058 Test: Used adb commands. Video in bug link. Change-Id: I5588a0e1dba4977074042a44bf358aa6ca3c9a20 --- .../res/layout/media_ttt_chip_receiver.xml | 33 ++++--- .../MediaTttChipControllerReceiver.kt | 88 ++++++++++++++----- 2 files changed, 87 insertions(+), 34 deletions(-) diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml index 4483db8aeb6fb..1daff9f70b88d 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml @@ -27,22 +27,27 @@ android:layout_height="wrap_content" /> - + > + - - + + + diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 8000cd84b151a..fab8c068b2a75 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -16,7 +16,9 @@ package com.android.systemui.media.taptotransfer.receiver +import android.animation.TimeInterpolator import android.annotation.SuppressLint +import android.animation.ValueAnimator import android.app.StatusBarManager import android.content.Context import android.graphics.Rect @@ -31,8 +33,10 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE +import android.view.View.ACCESSIBILITY_LIVE_REGION_NONE import com.android.internal.widget.CachingIconView import com.android.systemui.R +import com.android.systemui.animation.Interpolators import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.ui.binder.TintedIconViewBinder import com.android.systemui.dagger.SysUISingleton @@ -101,6 +105,13 @@ open class MediaTttChipControllerReceiver @Inject constructor( fitInsetsTypes = 0 // Ignore insets from all system bars } + // Value animator that controls the bouncing animation of views. + private val bounceAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + repeatCount = ValueAnimator.INFINITE + repeatMode = ValueAnimator.REVERSE + duration = ICON_BOUNCE_ANIM_DURATION + } + private val commandQueueCallbacks = object : CommandQueue.Callbacks { override fun updateMediaTapToTransferReceiverDisplay( @StatusBarManager.MediaTransferReceiverState displayState: Int, @@ -203,44 +214,52 @@ open class MediaTttChipControllerReceiver @Inject constructor( val iconView = currentView.getAppIconView() iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding) - iconView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE TintedIconViewBinder.bind(iconInfo.toTintedIcon(), iconView) + + val iconContainerView = currentView.getIconContainerView() + iconContainerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE } override fun animateViewIn(view: ViewGroup) { - val appIconView = view.getAppIconView() + val iconContainerView = view.getIconContainerView() val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple) val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple) - animateViewTranslationAndFade(appIconView, -1 * getTranslationAmount(), 1f) - animateViewTranslationAndFade(iconRippleView, -1 * getTranslationAmount(), 1f) + val translationYBy = getTranslationAmount() + // Make the icon container view starts animation from bottom of the screen. + iconContainerView.translationY += rippleController.getReceiverIconSize() + animateViewTranslationAndFade( + iconContainerView, + translationYBy = -1 * translationYBy, + alphaEndValue = 1f, + Interpolators.EMPHASIZED_DECELERATE, + ) { + animateBouncingView(iconContainerView, translationYBy * BOUNCE_TRANSLATION_RATIO) + } rippleController.expandToInProgressState(rippleView, iconRippleView) } override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) { - val appIconView = view.getAppIconView() - val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple) + val iconContainerView = view.getIconContainerView() val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple) + val translationYBy = getTranslationAmount() + + // Remove update listeners from bounce animator to prevent any conflict with + // translation animation. + bounceAnimator.removeAllUpdateListeners() + bounceAnimator.cancel() if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name && mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) { rippleController.expandToSuccessState(rippleView, onAnimationEnd) animateViewTranslationAndFade( - iconRippleView, - -1 * getTranslationAmount(), - 0f, - translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION, - alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION, - ) - animateViewTranslationAndFade( - appIconView, - -1 * getTranslationAmount(), + iconContainerView, + -1 * translationYBy, 0f, translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION, alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION, ) } else { rippleController.collapseRipple(rippleView, onAnimationEnd) - animateViewTranslationAndFade(iconRippleView, getTranslationAmount(), 0f) - animateViewTranslationAndFade(appIconView, getTranslationAmount(), 0f) + animateViewTranslationAndFade(iconContainerView, translationYBy, 0f) } } @@ -252,15 +271,19 @@ open class MediaTttChipControllerReceiver @Inject constructor( /** Animation of view translation and fading. */ private fun animateViewTranslationAndFade( - view: View, + view: ViewGroup, translationYBy: Float, alphaEndValue: Float, + interpolator: TimeInterpolator? = null, translationDuration: Long = ICON_TRANSLATION_ANIM_DURATION, alphaDuration: Long = ICON_ALPHA_ANIM_DURATION, + onAnimationEnd: Runnable? = null, ) { view.animate() .translationYBy(translationYBy) + .setInterpolator(interpolator) .setDuration(translationDuration) + .withEndAction { onAnimationEnd?.run() } .start() view.animate() .alpha(alphaEndValue) @@ -270,17 +293,42 @@ open class MediaTttChipControllerReceiver @Inject constructor( /** Returns the amount that the chip will be translated by in its intro animation. */ private fun getTranslationAmount(): Float { - return rippleController.getRippleSize() * 0.5f - - rippleController.getReceiverIconSize() + return rippleController.getRippleSize() * 0.5f } private fun View.getAppIconView(): CachingIconView { return this.requireViewById(R.id.app_icon) } + private fun View.getIconContainerView(): ViewGroup { + return this.requireViewById(R.id.icon_container_view) + } + + private fun animateBouncingView(iconContainerView: ViewGroup, translationYBy: Float) { + if (bounceAnimator.isStarted) { + return + } + + addViewToBounceAnimation(iconContainerView, translationYBy) + + // In order not to announce description every time the view animate. + iconContainerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE + bounceAnimator.start() + } + + private fun addViewToBounceAnimation(view: View, translationYBy: Float) { + val prevTranslationY = view.translationY + bounceAnimator.addUpdateListener { updateListener -> + val progress = updateListener.animatedValue as Float + view.translationY = prevTranslationY + translationYBy * progress + } + } + companion object { private const val ICON_TRANSLATION_ANIM_DURATION = 500L + private const val ICON_BOUNCE_ANIM_DURATION = 750L private const val ICON_TRANSLATION_SUCCEEDED_DURATION = 167L + private const val BOUNCE_TRANSLATION_RATIO = 0.15f private val ICON_ALPHA_ANIM_DURATION = 5.frames } }