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/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml index e079fd3c5e8f0..21d12c2784532 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml @@ -29,6 +29,7 @@ 100dp 95dp - - 70dp + + 12dp 20dp diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index 792ae7ca60491..c3de94f28aea1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -19,7 +19,6 @@ package com.android.systemui.media.taptotransfer.common import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.Drawable -import com.android.internal.widget.CachingIconView import com.android.settingslib.Utils import com.android.systemui.R @@ -76,29 +75,6 @@ class MediaTttUtils { isAppIcon = false ) } - - /** - * Sets an icon to be displayed by the given view. - * - * @param iconSize the size in pixels that the icon should be. If null, the size of - * [appIconView] will not be adjusted. - */ - fun setIcon( - appIconView: CachingIconView, - icon: Drawable, - iconContentDescription: CharSequence, - iconSize: Int? = null, - ) { - iconSize?.let { size -> - val lp = appIconView.layoutParams - lp.width = size - lp.height = size - appIconView.layoutParams = lp - } - - appIconView.contentDescription = iconContentDescription - appIconView.setImageDrawable(icon) - } } } 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 dfd9e22c14b14..8fc5519cc73e7 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 @@ -30,6 +30,7 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import com.android.internal.widget.CachingIconView import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton @@ -146,20 +147,17 @@ class MediaTttChipControllerReceiver @Inject constructor( ) val iconDrawable = newInfo.appIconDrawableOverride ?: iconInfo.drawable val iconContentDescription = newInfo.appNameOverride ?: iconInfo.contentDescription - val iconSize = context.resources.getDimensionPixelSize( + val iconPadding = if (iconInfo.isAppIcon) { - R.dimen.media_ttt_icon_size_receiver + 0 } else { - R.dimen.media_ttt_generic_icon_size_receiver + context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding) } - ) - MediaTttUtils.setIcon( - currentView.requireViewById(R.id.app_icon), - iconDrawable, - iconContentDescription, - iconSize, - ) + val iconView = currentView.requireViewById(R.id.app_icon) + iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding) + iconView.setImageDrawable(iconDrawable) + iconView.contentDescription = iconContentDescription } override fun animateViewIn(view: ViewGroup) { 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 007eb8f8deee1..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 @@ -29,6 +29,7 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.TextView import com.android.internal.statusbar.IUndoMediaTransferCallback +import com.android.internal.widget.CachingIconView import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.animation.Interpolators @@ -53,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, @@ -145,11 +146,9 @@ class MediaTttChipControllerSender @Inject constructor( val iconInfo = MediaTttUtils.getIconInfoFromPackageName( context, newInfo.routeInfo.clientPackageName, logger ) - MediaTttUtils.setIcon( - currentView.requireViewById(R.id.app_icon), - iconInfo.drawable, - iconInfo.contentDescription - ) + val iconView = currentView.requireViewById(R.id.app_icon) + iconView.setImageDrawable(iconInfo.drawable) + iconView.contentDescription = iconInfo.contentDescription // Text val otherDeviceName = newInfo.routeInfo.name.toString() @@ -196,7 +195,19 @@ class MediaTttChipControllerSender @Inject constructor( ) } - override fun removeView(removalReason: String) { + 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.) val transferStatus = info?.state?.transferStatus @@ -208,9 +219,9 @@ class MediaTttChipControllerSender @Inject constructor( logger.logRemovalBypass( removalReason, bypassReason = "transferStatus=${transferStatus.name}" ) - return + return true } - super.removeView(removalReason) + return false } private fun Boolean.visibleIfTrue(): Int { diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index a52e2aff52c18..91e20ee309762 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -167,17 +167,32 @@ 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" diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt index 921b7efc38eba..7cb28068fe6cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt @@ -61,6 +61,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { @Mock private lateinit var powerManager: PowerManager + private var shouldIgnoreViewRemoval: Boolean = false + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -205,6 +207,26 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { verify(windowManager, never()).removeView(any()) } + @Test + fun removeView_shouldIgnoreRemovalFalse_viewRemoved() { + shouldIgnoreViewRemoval = false + underTest.displayView(getState()) + + underTest.removeView("reason") + + verify(windowManager).removeView(any()) + } + + @Test + fun removeView_shouldIgnoreRemovalTrue_viewNotRemoved() { + shouldIgnoreViewRemoval = true + underTest.displayView(getState()) + + underTest.removeView("reason") + + verify(windowManager, never()).removeView(any()) + } + private fun getState(name: String = "name") = ViewInfo(name) private fun getConfigurationListener(): ConfigurationListener { @@ -240,6 +262,10 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { super.updateView(newInfo, currentView) mostRecentViewInfo = newInfo } + + override fun shouldIgnoreViewRemoval(removalReason: String): Boolean { + return shouldIgnoreViewRemoval + } } inner class ViewInfo(val name: String) : TemporaryViewInfo {