From 9f1fc82e187ec71cf1afc93e97d7c22d8e5ab9a4 Mon Sep 17 00:00:00 2001 From: Caitlin Cassidy Date: Mon, 16 May 2022 20:41:46 +0000 Subject: [PATCH] [Media TTT] Animate the chip in. This includes updating ViewHierarchyAnimator to also handle fading in during #animateAddition (if requested). Bug: 203800644 Test: manual: See fast + slow animations of the chip attached to the bug. Test: ViewHierarchyAnimatorTest Change-Id: Iedf29695b187eaeddc8734d326ecf08a0ad63dcb --- .../SystemUI/animation/res/values/ids.xml | 1 + .../animation/ViewHierarchyAnimator.kt | 66 +++++++- .../SystemUI/res/layout/media_ttt_chip.xml | 126 ++++++++------ .../common/MediaTttChipControllerCommon.kt | 7 + .../sender/MediaTttChipControllerSender.kt | 14 +- .../animation/ViewHierarchyAnimatorTest.kt | 159 ++++++++++++++++++ .../MediaTttChipControllerSenderTest.kt | 16 +- 7 files changed, 323 insertions(+), 66 deletions(-) diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml index 03ca462beb768..f7150ab548dde 100644 --- a/packages/SystemUI/animation/res/values/ids.xml +++ b/packages/SystemUI/animation/res/values/ids.xml @@ -21,6 +21,7 @@ + diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt index 093589f8c636a..4c3cd3c2b4412 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt @@ -39,6 +39,7 @@ class ViewHierarchyAnimator { private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE + private val DEFAULT_FADE_IN_INTERPOLATOR = Interpolators.ALPHA_IN /** The properties used to animate the view bounds. */ private val PROPERTIES = mapOf( @@ -162,6 +163,10 @@ class ViewHierarchyAnimator { * animate an already visible view, see [animate] and [animateNextUpdate]. * * Then animator unregisters itself once the first addition animation is complete. + * + * @param includeFadeIn true if the animator should also fade in the view and child views. + * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if + * [includeFadeIn] is false. */ @JvmOverloads fun animateAddition( @@ -169,7 +174,9 @@ class ViewHierarchyAnimator { origin: Hotspot = Hotspot.CENTER, interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR, duration: Long = DEFAULT_DURATION, - includeMargins: Boolean = false + includeMargins: Boolean = false, + includeFadeIn: Boolean = false, + fadeInInterpolator: Interpolator = DEFAULT_FADE_IN_INTERPOLATOR ): Boolean { if (isVisible( rootView.visibility, @@ -186,6 +193,42 @@ class ViewHierarchyAnimator { origin, interpolator, duration, ignorePreviousValues = !includeMargins ) addListener(rootView, listener, recursive = true) + + if (!includeFadeIn) { + return true + } + + if (rootView is ViewGroup) { + // First, fade in the container view + val containerDuration = duration / 6 + createAndStartFadeInAnimator( + rootView, containerDuration, startDelay = 0, interpolator = fadeInInterpolator + ) + + // Then, fade in the child views + val childDuration = duration / 3 + for (i in 0 until rootView.childCount) { + val view = rootView.getChildAt(i) + createAndStartFadeInAnimator( + view, + childDuration, + // Wait until the container fades in before fading in the children + startDelay = containerDuration, + interpolator = fadeInInterpolator + ) + } + // For now, we don't recursively fade in additional sub views (e.g. grandchild + // views) since it hasn't been necessary, but we could add that functionality. + } else { + // Fade in the view during the first half of the addition + createAndStartFadeInAnimator( + rootView, + duration / 2, + startDelay = 0, + interpolator = fadeInInterpolator + ) + } + return true } @@ -834,6 +877,27 @@ class ViewHierarchyAnimator { view.setTag(R.id.tag_animator, animator) animator.start() } + + private fun createAndStartFadeInAnimator( + view: View, + duration: Long, + startDelay: Long, + interpolator: Interpolator + ) { + val animator = ObjectAnimator.ofFloat(view, "alpha", 1f) + animator.startDelay = startDelay + animator.duration = duration + animator.interpolator = interpolator + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + view.setTag(R.id.tag_alpha_animator, null /* tag */) + } + }) + + (view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel() + view.setTag(R.id.tag_alpha_animator, animator) + animator.start() + } } /** An enum used to determine the origin of addition animations. */ diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml index a502d33a0be14..4d24140abbf45 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip.xml @@ -13,71 +13,85 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + + + android:layout_height="wrap_content"> - - - + android:padding="@dimen/media_ttt_chip_outer_padding" + android:background="@drawable/media_ttt_chip_background" + android:layout_marginTop="20dp" + android:clipToPadding="false" + android:gravity="center_vertical" + android:alpha="0.0" + > - + - + - + + - + - + + + + diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt index 54b0c13456010..7cc52e4282189 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt @@ -106,6 +106,7 @@ abstract class MediaTttChipControllerCommon( PowerManager.WAKE_REASON_APPLICATION, "com.android.systemui:media_tap_to_transfer_activated" ) + animateChipIn(currentChipView) } // Cancel and re-set the chip timeout each time we get a new state. @@ -137,6 +138,12 @@ abstract class MediaTttChipControllerCommon( */ abstract fun updateChipView(chipInfo: T, currentChipView: ViewGroup) + /** + * A method that can be implemented by subclcasses to do custom animations for when the chip + * appears. + */ + open fun animateChipIn(chipView: ViewGroup) {} + /** * Returns the size that the icon should be, or null if no size override is needed. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index 9f5ec7e1a3306..54b4380e2443a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -27,6 +27,8 @@ import android.view.WindowManager import android.widget.TextView import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R +import com.android.systemui.animation.Interpolators +import com.android.systemui.animation.ViewHierarchyAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.common.ChipInfoCommon @@ -124,7 +126,6 @@ class MediaTttChipControllerSender @Inject constructor( currentChipView.requireViewById(R.id.loading).visibility = chipState.isMidTransfer.visibleIfTrue() - // Undo val undoView = currentChipView.requireViewById(R.id.undo) val undoClickListener = chipState.undoClickListener( @@ -138,6 +139,17 @@ class MediaTttChipControllerSender @Inject constructor( chipState.isTransferFailure.visibleIfTrue() } + override fun animateChipIn(chipView: ViewGroup) { + ViewHierarchyAnimator.animateAddition( + chipView.requireViewById(R.id.media_ttt_sender_chip_inner), + ViewHierarchyAnimator.Hotspot.TOP, + Interpolators.EMPHASIZED_DECELERATE, + duration = 500L, + includeMargins = true, + includeFadeIn = true, + ) + } + override fun removeChip(removalReason: String) { // Don't remove the chip if we're mid-transfer since the user should still be able to // see the status of the transfer. (But do remove it if it's finally timed out.) diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt index 6a9bb3e343bee..b61fbbe1ea75d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt @@ -520,6 +520,145 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { endAnimation(rootView) } + @Test + fun animatesAppearingViewsFadeIn_alphaStartsAtZero_endsAtOne() { + rootView.alpha = 0f + ViewHierarchyAnimator.animateAddition(rootView, includeFadeIn = true) + rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) + + advanceFadeInAnimation(rootView, fraction = 1f) + endFadeInAnimation(rootView) + + assertNull(rootView.getTag(R.id.tag_alpha_animator)) + assertEquals(1f, rootView.alpha) + } + + @Test + fun animatesAppearingViewsFadeIn_alphaStartsAboveZero_endsAtOne() { + rootView.alpha = 0.2f + ViewHierarchyAnimator.animateAddition(rootView, includeFadeIn = true) + rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) + + advanceFadeInAnimation(rootView, fraction = 1f) + endFadeInAnimation(rootView) + + assertNull(rootView.getTag(R.id.tag_alpha_animator)) + assertEquals(1f, rootView.alpha) + } + + @Test + fun animatesAppearingViewsFadeIn_alphaStartsAsZero_alphaUpdatedMidAnimation() { + rootView.alpha = 0f + ViewHierarchyAnimator.animateAddition( + rootView, + includeFadeIn = true, + fadeInInterpolator = Interpolators.LINEAR + ) + rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) + + advanceFadeInAnimation(rootView, fraction = 0.42f) + + assertEquals(0.42f, rootView.alpha) + } + + @Test + fun animatesAppearingViewsFadeIn_alphaStartsAboveZero_alphaUpdatedMidAnimation() { + rootView.alpha = 0.6f + ViewHierarchyAnimator.animateAddition( + rootView, + includeFadeIn = true, + fadeInInterpolator = Interpolators.LINEAR + ) + rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) + + advanceFadeInAnimation(rootView, fraction = 0.5f) + + assertEquals(0.8f, rootView.alpha) + } + + @Test + fun animatesAppearingViewsFadeIn_childViewAlphasAlsoAnimated() { + rootView.alpha = 0f + val firstChild = View(context) + firstChild.alpha = 0f + val secondChild = View(context) + secondChild.alpha = 0f + rootView.addView(firstChild) + rootView.addView(secondChild) + + ViewHierarchyAnimator.animateAddition( + rootView, + includeFadeIn = true, + fadeInInterpolator = Interpolators.LINEAR + ) + rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) + + advanceFadeInAnimation(rootView, fraction = 0.5f) + + assertEquals(0.5f, rootView.alpha) + assertEquals(0.5f, firstChild.alpha) + assertEquals(0.5f, secondChild.alpha) + } + + @Test + fun animatesAppearingViewsFadeIn_animatesFromPreviousAnimationProgress() { + rootView.alpha = 0f + ViewHierarchyAnimator.animateAddition( + rootView, + includeFadeIn = true, + fadeInInterpolator = Interpolators.LINEAR + ) + rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) + + advanceFadeInAnimation(rootView, fraction = 0.5f) + assertEquals(0.5f, rootView.alpha) + assertNotNull(rootView.getTag(R.id.tag_alpha_animator)) + + // IF we request animation again + ViewHierarchyAnimator.animateAddition( + rootView, + includeFadeIn = true, + fadeInInterpolator = Interpolators.LINEAR + ) + + // THEN the alpha remains at its current value (it doesn't get reset to 0) + assertNotNull(rootView.getTag(R.id.tag_alpha_animator)) + assertEquals(0.5f, rootView.alpha) + + // IF we advance the new animation to the end + advanceFadeInAnimation(rootView, fraction = 1f) + endFadeInAnimation(rootView) + + // THEN we still end at the correct value + assertNull(rootView.getTag(R.id.tag_alpha_animator)) + assertEquals(1f, rootView.alpha) + } + + @Test + fun animatesAppearingViews_fadeInFalse_alphasNotUpdated() { + rootView.alpha = 0.3f + val firstChild = View(context) + firstChild.alpha = 0.4f + val secondChild = View(context) + secondChild.alpha = 0.5f + rootView.addView(firstChild) + rootView.addView(secondChild) + + ViewHierarchyAnimator.animateAddition( + rootView, + includeFadeIn = false, + fadeInInterpolator = Interpolators.LINEAR + ) + rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) + + advanceFadeInAnimation(rootView, fraction = 1f) + + assertEquals(0.3f, rootView.alpha) + assertEquals(0.4f, firstChild.alpha) + assertEquals(0.5f, secondChild.alpha) + } + + @Test fun animatesViewRemovalFromStartToEnd() { setUpRootWithChildren() @@ -1003,6 +1142,16 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } } + private fun advanceFadeInAnimation(rootView: View, fraction: Float) { + (rootView.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.setCurrentFraction(fraction) + + if (rootView is ViewGroup) { + for (i in 0 until rootView.childCount) { + advanceFadeInAnimation(rootView.getChildAt(i), fraction) + } + } + } + private fun endAnimation(rootView: View) { (rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.end() @@ -1012,4 +1161,14 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } } } + + private fun endFadeInAnimation(rootView: View) { + (rootView.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.end() + + if (rootView is ViewGroup) { + for (i in 0 until rootView.childCount) { + endFadeInAnimation(rootView.getChildAt(i)) + } + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt index 9a01464fc8699..a8c72ddfd5d72 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt @@ -25,9 +25,9 @@ import android.os.PowerManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View +import android.view.ViewGroup import android.view.WindowManager import android.widget.ImageView -import android.widget.LinearLayout import android.widget.TextView import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake @@ -620,22 +620,22 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { verify(windowManager).removeView(any()) } - private fun LinearLayout.getAppIconView() = this.requireViewById(R.id.app_icon) + private fun ViewGroup.getAppIconView() = this.requireViewById(R.id.app_icon) - private fun LinearLayout.getChipText(): String = + private fun ViewGroup.getChipText(): String = (this.requireViewById(R.id.text)).text as String - private fun LinearLayout.getLoadingIconVisibility(): Int = + private fun ViewGroup.getLoadingIconVisibility(): Int = this.requireViewById(R.id.loading).visibility - private fun LinearLayout.getUndoButton(): View = this.requireViewById(R.id.undo) + private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo) - private fun LinearLayout.getFailureIcon(): View = this.requireViewById(R.id.failure_icon) + private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon) - private fun getChipView(): LinearLayout { + private fun getChipView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) - return viewCaptor.value as LinearLayout + return viewCaptor.value as ViewGroup } /** Helper method providing default parameters to not clutter up the tests. */