Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0f4c1710 authored by Ats Jenk's avatar Ats Jenk
Browse files

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
parent 285a32f1
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -294,6 +294,8 @@
    <dimen name="bubble_bar_expanded_view_drop_target_padding_top">60dp</dimen>
    <dimen name="bubble_bar_expanded_view_drop_target_padding_bottom">24dp</dimen>
    <dimen name="bubble_bar_expanded_view_drop_target_padding_horizontal">48dp</dimen>
    <dimen name="bubble_bar_drop_target_width">84dp</dimen>
    <dimen name="bubble_bar_drop_target_height">48dp</dimen>
    <!-- Width of the box around bottom center of the screen where drag only leads to dismiss -->
    <dimen name="bubble_bar_dismiss_zone_width">192dp</dimen>
    <!-- Height of the box around bottom center of the screen where drag only leads to dismiss -->
+5 −0
Original line number Diff line number Diff line
@@ -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
+20 −0
Original line number Diff line number Diff line
@@ -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;
    }
}
+110 −19
Original line number Diff line number Diff line
@@ -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,10 +194,10 @@ constructor(
                )
            } else {
                val animStartType = IndicatorType.valueOf(currentType.name)
                val animator =
                    indicatorView?.let {
                val indicator = indicatorView ?: return@execute
                var animator: Animator =
                    VisualIndicatorAnimator.animateIndicatorType(
                            it,
                        indicator,
                        layout,
                        animStartType,
                        newType,
@@ -194,12 +205,39 @@ constructor(
                        taskInfo.displayId,
                        snapEventHandler,
                    )
                    } ?: return@execute
                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
    }
}
+110 −0
Original line number Diff line number Diff line
@@ -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)
    }
}