diff options
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 03ca462beb76..f7150ab548dd 100644 --- a/packages/SystemUI/animation/res/values/ids.xml +++ b/packages/SystemUI/animation/res/values/ids.xml @@ -21,6 +21,7 @@ <!-- ViewBoundsAnimator --> <item type="id" name="tag_animator"/> + <item type="id" name="tag_alpha_animator"/> <item type="id" name="tag_layout_listener"/> <item type="id" name="tag_override_bottom"/> <item type="id" name="tag_override_left"/> 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 093589f8c636..4c3cd3c2b441 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 a502d33a0be1..4d24140abbf4 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. --> -<LinearLayout +<!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view + is the root view of a window, we cannot change the root view's margins.) --> +<!-- Alphas start as 0 because the view will be animated in. --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/media_ttt_sender_chip" - android:orientation="horizontal" android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="@dimen/media_ttt_chip_outer_padding" - android:background="@drawable/media_ttt_chip_background" - android:layout_marginTop="50dp" - android:clipToPadding="false" - android:gravity="center_vertical" - > + android:layout_height="wrap_content"> - <com.android.internal.widget.CachingIconView - android:id="@+id/app_icon" - android:layout_width="@dimen/media_ttt_app_icon_size" - android:layout_height="@dimen/media_ttt_app_icon_size" - android:layout_marginEnd="12dp" - /> - - <TextView - android:id="@+id/text" + <LinearLayout + android:id="@+id/media_ttt_sender_chip_inner" + android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="@dimen/media_ttt_text_size" - android:textColor="?android:attr/textColorPrimary" - /> + 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" + > - <!-- At most one of [loading, failure_icon, undo] will be visible at a time. --> + <com.android.internal.widget.CachingIconView + android:id="@+id/app_icon" + android:layout_width="@dimen/media_ttt_app_icon_size" + android:layout_height="@dimen/media_ttt_app_icon_size" + android:layout_marginEnd="12dp" + android:alpha="0.0" + /> - <ProgressBar - android:id="@+id/loading" - android:indeterminate="true" - android:layout_width="@dimen/media_ttt_status_icon_size" - android:layout_height="@dimen/media_ttt_status_icon_size" - android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" - android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant" - style="?android:attr/progressBarStyleSmall" - /> + <TextView + android:id="@+id/text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="@dimen/media_ttt_text_size" + android:textColor="?android:attr/textColorPrimary" + android:alpha="0.0" + /> - <ImageView - android:id="@+id/failure_icon" - android:layout_width="@dimen/media_ttt_status_icon_size" - android:layout_height="@dimen/media_ttt_status_icon_size" - android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" - android:src="@drawable/ic_warning" - android:tint="@color/GM2_red_500" - /> + <!-- At most one of [loading, failure_icon, undo] will be visible at a time. --> + <ProgressBar + android:id="@+id/loading" + android:indeterminate="true" + android:layout_width="@dimen/media_ttt_status_icon_size" + android:layout_height="@dimen/media_ttt_status_icon_size" + android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" + android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant" + style="?android:attr/progressBarStyleSmall" + android:alpha="0.0" + /> - <TextView - android:id="@+id/undo" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/media_transfer_undo" - android:textColor="?androidprv:attr/textColorOnAccent" - android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" - android:textSize="@dimen/media_ttt_text_size" - android:paddingStart="@dimen/media_ttt_chip_outer_padding" - android:paddingEnd="@dimen/media_ttt_chip_outer_padding" - android:paddingTop="@dimen/media_ttt_undo_button_vertical_padding" - android:paddingBottom="@dimen/media_ttt_undo_button_vertical_padding" - android:layout_marginTop="@dimen/media_ttt_undo_button_vertical_negative_margin" - android:layout_marginBottom="@dimen/media_ttt_undo_button_vertical_negative_margin" - android:background="@drawable/media_ttt_undo_background" - /> + <ImageView + android:id="@+id/failure_icon" + android:layout_width="@dimen/media_ttt_status_icon_size" + android:layout_height="@dimen/media_ttt_status_icon_size" + android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" + android:src="@drawable/ic_warning" + android:tint="@color/GM2_red_500" + android:alpha="0.0" + /> + + <TextView + android:id="@+id/undo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/media_transfer_undo" + android:textColor="?androidprv:attr/textColorOnAccent" + android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" + android:textSize="@dimen/media_ttt_text_size" + android:paddingStart="@dimen/media_ttt_chip_outer_padding" + android:paddingEnd="@dimen/media_ttt_chip_outer_padding" + android:paddingTop="@dimen/media_ttt_undo_button_vertical_padding" + android:paddingBottom="@dimen/media_ttt_undo_button_vertical_padding" + android:layout_marginTop="@dimen/media_ttt_undo_button_vertical_negative_margin" + android:layout_marginBottom="@dimen/media_ttt_undo_button_vertical_negative_margin" + android:background="@drawable/media_ttt_undo_background" + android:alpha="0.0" + /> -</LinearLayout> + </LinearLayout> +</FrameLayout> 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 54b0c1345601..7cc52e428218 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<T : ChipInfoCommon>( 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. @@ -138,6 +139,12 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( 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. */ open fun getIconSize(isAppIcon: Boolean): Int? = null 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 9f5ec7e1a330..54b4380e2443 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<View>(R.id.loading).visibility = chipState.isMidTransfer.visibleIfTrue() - // Undo val undoView = currentChipView.requireViewById<View>(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<ViewGroup>(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 6a9bb3e343be..b61fbbe1ea75 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 9a01464fc869..a8c72ddfd5d7 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<ImageView>(R.id.app_icon) + private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon) - private fun LinearLayout.getChipText(): String = + private fun ViewGroup.getChipText(): String = (this.requireViewById<TextView>(R.id.text)).text as String - private fun LinearLayout.getLoadingIconVisibility(): Int = + private fun ViewGroup.getLoadingIconVisibility(): Int = this.requireViewById<View>(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. */ |