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 dc2c63561d79c..1b7e26b0aea09 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt @@ -361,13 +361,17 @@ class ViewHierarchyAnimator { * * The end state of the animation is controlled by [destination]. This value can be any of * the four corners, any of the four edges, or the center of the view. + * + * @param onAnimationEnd an optional runnable that will be run once the animation finishes + * successfully. Will not be run if the animation is cancelled. */ @JvmOverloads fun animateRemoval( rootView: View, destination: Hotspot = Hotspot.CENTER, interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR, - duration: Long = DEFAULT_DURATION + duration: Long = DEFAULT_DURATION, + onAnimationEnd: Runnable? = null, ): Boolean { if ( !occupiesSpace( @@ -391,13 +395,28 @@ class ViewHierarchyAnimator { addListener(child, listener, recursive = false) } - // Remove the view so that a layout update is triggered for the siblings and they - // animate to their next position while the view's removal is also animating. - parent.removeView(rootView) - // By adding the view to the overlay, we can animate it while it isn't part of the view - // hierarchy. It is correctly positioned because we have its previous bounds, and we set - // them manually during the animation. - parent.overlay.add(rootView) + val viewHasSiblings = parent.childCount > 1 + if (viewHasSiblings) { + // Remove the view so that a layout update is triggered for the siblings and they + // animate to their next position while the view's removal is also animating. + parent.removeView(rootView) + // By adding the view to the overlay, we can animate it while it isn't part of the + // view hierarchy. It is correctly positioned because we have its previous bounds, + // and we set them manually during the animation. + parent.overlay.add(rootView) + } + // If this view has no siblings, the parent view may shrink to (0,0) size and mess + // up the animation if we immediately remove the view. So instead, we just leave the + // view in the real hierarchy until the animation finishes. + + val endRunnable = Runnable { + if (viewHasSiblings) { + parent.overlay.remove(rootView) + } else { + parent.removeView(rootView) + } + onAnimationEnd?.run() + } val startValues = mapOf( @@ -430,7 +449,8 @@ class ViewHierarchyAnimator { endValues, interpolator, duration, - ephemeral = true + ephemeral = true, + endRunnable, ) if (rootView is ViewGroup) { @@ -463,7 +483,6 @@ class ViewHierarchyAnimator { .alpha(0f) .setInterpolator(Interpolators.ALPHA_OUT) .setDuration(duration / 2) - .withEndAction { parent.overlay.remove(rootView) } .start() } } @@ -477,7 +496,6 @@ class ViewHierarchyAnimator { .setInterpolator(Interpolators.ALPHA_OUT) .setDuration(duration / 2) .setStartDelay(duration / 2) - .withEndAction { parent.overlay.remove(rootView) } .start() } 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 71389f55d360b..11c55285c00b3 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 @@ -54,7 +54,7 @@ import javax.inject.Inject * chip is shown when a user is transferring media to/from this device and a receiver device. */ @SysUISingleton -class MediaTttChipControllerSender @Inject constructor( +open class MediaTttChipControllerSender @Inject constructor( commandQueue: CommandQueue, context: Context, @MediaTttSenderLogger logger: MediaTttLogger, @@ -195,6 +195,18 @@ class MediaTttChipControllerSender @Inject constructor( ) } + override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { + ViewHierarchyAnimator.animateRemoval( + view.requireViewById(R.id.media_ttt_sender_chip_inner), + ViewHierarchyAnimator.Hotspot.TOP, + Interpolators.EMPHASIZED_ACCELERATE, + ANIMATION_DURATION, + onAnimationEnd, + ) + // TODO(b/203800644): Add includeMargins as an option to ViewHierarchyAnimator so that the + // animateChipOut matches the animateChipIn. + } + override fun shouldIgnoreViewRemoval(removalReason: String): Boolean { // Don't remove the chip if we're in progress or succeeded, 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/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index dc9a683c83c2a..91e20ee309762 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -171,11 +171,15 @@ abstract class TemporaryViewDisplayController, + falsingCollector: Lazy, + ) : MediaTttChipControllerSender( + commandQueue, + context, + logger, + windowManager, + mainExecutor, + accessibilityManager, + configurationController, + powerManager, + uiEventLogger, + falsingManager, + falsingCollector, + ) { + override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { + // Just bypass the animation in tests + onAnimationEnd.run() + } + } } private const val APP_NAME = "Fake app name"