From 0f4c171019b7365f993e90c1a532ed69ca86f779 Mon Sep 17 00:00:00 2001 From: Ats Jenk Date: Tue, 11 Mar 2025 09:36:33 -0700 Subject: Include bar outline for bubble drag indicators When dragging a task to bubble we show the expanded view hint as the drop target. We also need to include an oval for the bubble bar to better emphasize that the drop in this area will lead to task being converted to a bubble. The existing indicator view is based on a single View and we modify the bounds of the backgroud drawable. If bubbles feature is enabled, use a FrameLayout for the View and add a child view that is for the bubble bar indicator. Animate bubble bar indicator together with the other indicator. Bug: 388856523 Test: atest VisualIndicatorViewContainerTest Flag: com.android.wm.shell.enable_bubble_to_fullscreen Change-Id: I892d10d2ce32271d3a502a301a7f342eb279085e --- libs/WindowManager/Shell/res/values/dimen.xml | 2 + .../bubbles/BubbleDropTargetBoundsProvider.kt | 5 + .../android/wm/shell/bubbles/BubblePositioner.java | 20 ++++ .../desktopmode/VisualIndicatorViewContainer.kt | 129 ++++++++++++++++++--- .../VisualIndicatorViewContainerTest.kt | 110 ++++++++++++++++++ 5 files changed, 247 insertions(+), 19 deletions(-) (limited to 'libs') diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index e88458961802..caf32e114b5b 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -294,6 +294,8 @@ 60dp 24dp 48dp + 84dp + 48dp 192dp diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt index 9bee11a92430..84e0fbe96de2 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt @@ -26,4 +26,9 @@ interface BubbleDropTargetBoundsProvider { * Get bubble bar expanded view visual drop target bounds on screen */ fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect + + /** + * Get the bar visual drop target bounds on screen + */ + fun getBarDropTargetBounds(onLeft: Boolean): Rect } \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 03d6b0a8075d..0b45b086e13c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -106,6 +106,8 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider { private int mBarExpViewDropTargetPaddingTop; private int mBarExpViewDropTargetPaddingBottom; private int mBarExpViewDropTargetPaddingHorizontal; + private int mBarDropTargetWidth; + private int mBarDropTargetHeight; private PointF mRestingStackPosition; @@ -181,6 +183,8 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider { R.dimen.bubble_bar_expanded_view_drop_target_padding_bottom); mBarExpViewDropTargetPaddingHorizontal = res.getDimensionPixelSize( R.dimen.bubble_bar_expanded_view_drop_target_padding_horizontal); + mBarDropTargetWidth = res.getDimensionPixelSize(R.dimen.bubble_bar_drop_target_width); + mBarDropTargetHeight = res.getDimensionPixelSize(R.dimen.bubble_bar_drop_target_height); if (mShowingInBubbleBar) { mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth; @@ -1003,4 +1007,20 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider { ); return bounds; } + + @NonNull + @Override + public Rect getBarDropTargetBounds(boolean onLeft) { + Rect bounds = getBubbleBarExpandedViewDropTargetBounds(onLeft); + bounds.top = getBubbleBarTopOnScreen(); + bounds.bottom = bounds.top + mBarDropTargetHeight; + if (onLeft) { + // Keep the left edge from expanded view + bounds.right = bounds.left + mBarDropTargetWidth; + } else { + // Keep the right edge from expanded view + bounds.left = bounds.right - mBarDropTargetWidth; + } + return bounds; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt index 23562388b3e5..5e4122ba14ec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet import android.animation.RectEvaluator import android.animation.ValueAnimator import android.app.ActivityManager @@ -32,6 +33,8 @@ import android.view.View import android.view.WindowManager import android.view.WindowlessWindowManager import android.view.animation.DecelerateInterpolator +import android.widget.FrameLayout +import androidx.core.animation.doOnEnd import com.android.internal.annotations.VisibleForTesting import com.android.window.flags.Flags import com.android.wm.shell.R @@ -42,6 +45,7 @@ import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.shared.annotations.ShellDesktopThread import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory import com.android.wm.shell.windowdecor.tiling.SnapEventHandler @@ -64,6 +68,8 @@ constructor( private val snapEventHandler: SnapEventHandler, ) { @VisibleForTesting var indicatorView: View? = null + // Optional extra indicator showing the outline of the bubble bar + private var barIndicatorView: View? = null private var indicatorViewHost: SurfaceControlViewHost? = null // Below variables and the SyncTransactionQueue are the only variables that should // be accessed from shell main thread. Everything else should be used exclusively @@ -93,7 +99,12 @@ constructor( screenWidth = metrics.widthPixels screenHeight = metrics.heightPixels } - indicatorView = View(context) + indicatorView = + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + FrameLayout(context) + } else { + View(context) + } val leash = indicatorBuilder .setName("Desktop Mode Visual Indicator") @@ -183,23 +194,50 @@ constructor( ) } else { val animStartType = IndicatorType.valueOf(currentType.name) - val animator = - indicatorView?.let { - VisualIndicatorAnimator.animateIndicatorType( - it, - layout, - animStartType, - newType, - bubbleBoundsProvider, - taskInfo.displayId, - snapEventHandler, - ) - } ?: return@execute + val indicator = indicatorView ?: return@execute + var animator: Animator = + VisualIndicatorAnimator.animateIndicatorType( + indicator, + layout, + animStartType, + newType, + bubbleBoundsProvider, + taskInfo.displayId, + snapEventHandler, + ) + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + if (currentType.isBubbleType() || newType.isBubbleType()) { + animator = addBarIndicatorAnimation(animator, currentType, newType) + } + } animator.start() } } } + private fun addBarIndicatorAnimation( + visualIndicatorAnimator: Animator, + currentType: IndicatorType, + newType: IndicatorType, + ): Animator { + if (newType.isBubbleType()) { + getOrCreateBubbleBarIndicator(newType)?.let { bar -> + return AnimatorSet().apply { + playTogether(visualIndicatorAnimator, fadeBarIndicatorIn(bar)) + } + } + } + if (currentType.isBubbleType()) { + barIndicatorView?.let { bar -> + barIndicatorView = null + return AnimatorSet().apply { + playTogether(visualIndicatorAnimator, fadeBarIndicatorOut(bar)) + } + } + } + return visualIndicatorAnimator + } + /** * Fade indicator in as provided type. * @@ -223,17 +261,20 @@ constructor( snapEventHandler: SnapEventHandler, ) { desktopExecutor.assertCurrentThread() - indicatorView?.let { - it.setBackgroundResource(R.drawable.desktop_windowing_transition_background) - val animator = + indicatorView?.let { indicator -> + indicator.setBackgroundResource(R.drawable.desktop_windowing_transition_background) + var animator: Animator = VisualIndicatorAnimator.fadeBoundsIn( - it, + indicator, type, layout, bubbleBoundsProvider, displayId, snapEventHandler, ) + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + animator = addBarIndicatorAnimation(animator, IndicatorType.NO_INDICATOR, type) + } animator.start() } } @@ -259,7 +300,7 @@ constructor( desktopExecutor.execute { indicatorView?.let { val animStartType = IndicatorType.valueOf(currentType.name) - val animator = + var animator: Animator = VisualIndicatorAnimator.fadeBoundsOut( it, animStartType, @@ -268,6 +309,10 @@ constructor( displayId, snapEventHandler, ) + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + animator = + addBarIndicatorAnimation(animator, currentType, IndicatorType.NO_INDICATOR) + } animator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { @@ -302,6 +347,38 @@ constructor( isReleased = true } + private fun getOrCreateBubbleBarIndicator(type: IndicatorType): View? { + val container = indicatorView as? FrameLayout ?: return null + val onLeft = type == IndicatorType.TO_BUBBLE_LEFT_INDICATOR + val bounds = bubbleBoundsProvider?.getBarDropTargetBounds(onLeft) ?: return null + val lp = FrameLayout.LayoutParams(bounds.width(), bounds.height()) + lp.leftMargin = bounds.left + lp.topMargin = bounds.top + if (barIndicatorView == null) { + val indicator = View(container.context) + indicator.setBackgroundResource(R.drawable.desktop_windowing_transition_background) + container.addView(indicator, lp) + barIndicatorView = indicator + } else { + barIndicatorView?.layoutParams = lp + } + return barIndicatorView + } + + private fun fadeBarIndicatorIn(barIndicator: View): Animator { + // Use layout bounds as the end bounds in case the view has not been laid out yet + val lp = barIndicator.layoutParams + val endBounds = Rect(0, 0, lp.width, lp.height) + return VisualIndicatorAnimator.fadeBoundsIn(barIndicator, endBounds) + } + + private fun fadeBarIndicatorOut(barIndicator: View): Animator { + val startBounds = Rect(0, 0, barIndicator.width, barIndicator.height) + val barAnimator = VisualIndicatorAnimator.fadeBoundsOut(barIndicator, startBounds) + barAnimator.doOnEnd { (indicatorView as? FrameLayout)?.removeView(barIndicator) } + return barAnimator + } + /** * Animator for Desktop Mode transitions which supports bounds and alpha animation. Functions * should only be called from the desktop executor. @@ -383,9 +460,13 @@ constructor( displayId, snapEventHandler, ) + return fadeBoundsIn(view, endBounds) + } + + @ShellDesktopThread + fun fadeBoundsIn(view: View, endBounds: Rect): VisualIndicatorAnimator { val startBounds = getMinBounds(endBounds) view.background.bounds = startBounds - val animator = VisualIndicatorAnimator(view, startBounds, endBounds) animator.interpolator = DecelerateInterpolator() setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM) @@ -409,6 +490,11 @@ constructor( displayId, snapEventHandler, ) + return fadeBoundsOut(view, startBounds) + } + + @ShellDesktopThread + fun fadeBoundsOut(view: View, startBounds: Rect): VisualIndicatorAnimator { val endBounds = getMinBounds(startBounds) view.background.bounds = startBounds val animator = VisualIndicatorAnimator(view, startBounds, endBounds) @@ -571,4 +657,9 @@ constructor( } } } + + private fun IndicatorType.isBubbleType(): Boolean { + return this == IndicatorType.TO_BUBBLE_LEFT_INDICATOR || + this == IndicatorType.TO_BUBBLE_RIGHT_INDICATOR + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt index c7518d5914b4..794fe311928c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode +import android.animation.AnimatorTestRule import android.app.ActivityManager import android.app.ActivityManager.RunningTaskInfo import android.graphics.Rect @@ -29,6 +30,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View +import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.wm.shell.ShellTestCase @@ -43,6 +45,7 @@ import com.android.wm.shell.windowdecor.tiling.SnapEventHandler import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.Before +import org.junit.Rule import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock @@ -67,6 +70,9 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class VisualIndicatorViewContainerTest : ShellTestCase() { + + @JvmField @Rule val animatorTestRule = AnimatorTestRule(this) + @Mock private lateinit var view: View @Mock private lateinit var displayLayout: DisplayLayout @Mock private lateinit var displayController: DisplayController @@ -297,6 +303,95 @@ class VisualIndicatorViewContainerTest : ShellTestCase() { verify(spyViewContainer, never()).fadeInIndicatorInternal(any(), any(), any(), any()) } + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testCreateView_bubblesEnabled_indicatorIsFrameLayout() { + val spyViewContainer = setupSpyViewContainer() + assertThat(spyViewContainer.indicatorView).isInstanceOf(FrameLayout::class.java) + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testFadeInOutBubbleIndicator_addAndRemoveBarIndicator() { + setUpBubbleBoundsProvider() + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.fadeInIndicator( + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + DEFAULT_DISPLAY, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(200) + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() + + spyViewContainer.fadeOutIndicator( + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + finishCallback = null, + DEFAULT_DISPLAY, + snapEventHandler, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(250) + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNull() + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testTransitionIndicator_fullscreenToBubble_addBarIndicator() { + setUpBubbleBoundsProvider() + val spyViewContainer = setupSpyViewContainer() + + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(200) + + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testTransitionIndicator_bubbleToFullscreen_removeBarIndicator() { + setUpBubbleBoundsProvider() + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.fadeInIndicator( + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + DEFAULT_DISPLAY, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(200) + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() + + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(200) + + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNull() + } + private fun setupSpyViewContainer(): VisualIndicatorViewContainer { val viewContainer = VisualIndicatorViewContainer( @@ -331,7 +426,22 @@ class VisualIndicatorViewContainerTest : ShellTestCase() { .build() } + private fun setUpBubbleBoundsProvider() { + bubbleDropTargetBoundsProvider = + object : BubbleDropTargetBoundsProvider { + override fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect { + return BUBBLE_INDICATOR_BOUNDS + } + + override fun getBarDropTargetBounds(onLeft: Boolean): Rect { + return BAR_INDICATOR_BOUNDS + } + } + } + companion object { private val DISPLAY_BOUNDS = Rect(0, 0, 1000, 1000) + private val BUBBLE_INDICATOR_BOUNDS = Rect(800, 200, 900, 900) + private val BAR_INDICATOR_BOUNDS = Rect(880, 950, 900, 960) } } -- cgit v1.2.3-59-g8ed1b