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

Commit e7d52f52 authored by Ats Jenk's avatar Ats Jenk Committed by Android (Google) Code Review
Browse files

Merge "Handle swipe down on collapsed bubble bar" into main

parents 63c24aea 0b19598d
Loading
Loading
Loading
Loading
+69 −26
Original line number Diff line number Diff line
@@ -21,10 +21,15 @@ import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.core.animation.doOnEnd
import androidx.dynamicanimation.animation.SpringForce
import com.android.launcher3.R
import com.android.launcher3.anim.AnimatedFloat
import com.android.launcher3.anim.SpringAnimationBuilder
import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.TaskbarThresholdUtils
import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.StartState.COLLAPSED
import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.StartState.EXPANDED
import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.StartState.STASHED
import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.StartState.UNKNOWN
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
import com.android.launcher3.touch.OverScroll

@@ -34,8 +39,8 @@ class BubbleBarSwipeController {
    private val context: Context

    private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null
    private var bubbleBarViewController: BubbleBarViewController? = null
    private var bubbleStashController: BubbleStashController? = null
    private lateinit var bubbleBarViewController: BubbleBarViewController
    private lateinit var bubbleStashController: BubbleStashController

    private var springAnimation: ValueAnimator? = null
    private val animatedSwipeTranslation = AnimatedFloat(this::onSwipeUpdate)
@@ -43,6 +48,7 @@ class BubbleBarSwipeController {
    private val unstashThreshold: Int
    private val expandThreshold: Int
    private val maxOverscroll: Int
    private val stashThreshold: Int

    private var swipeState: SwipeState = SwipeState()

@@ -54,6 +60,7 @@ class BubbleBarSwipeController {
        unstashThreshold = dimensionProvider.unstashThreshold
        expandThreshold = dimensionProvider.expandThreshold
        maxOverscroll = dimensionProvider.maxOverscroll
        stashThreshold = dimensionProvider.stashThreshold
    }

    fun init(bubbleControllers: BubbleControllers) {
@@ -66,23 +73,21 @@ class BubbleBarSwipeController {
    /** Start tracking a new swipe gesture */
    fun start() {
        if (springAnimation != null) reset()
        val stashed = bubbleStashController?.isStashed ?: false
        val barVisible = bubbleStashController?.isBubbleBarVisible() ?: false
        val expanded = bubbleBarViewController?.isExpanded ?: false

        swipeState =
            SwipeState(
                stashedOnStart = stashed,
                collapsedOnStart = !stashed && barVisible && !expanded,
                expandedOnStart = expanded,
            )
        val startState =
            when {
                bubbleStashController.isStashed -> STASHED
                bubbleBarViewController.isExpanded -> EXPANDED
                bubbleStashController.isBubbleBarVisible() -> COLLAPSED
                else -> UNKNOWN
            }
        swipeState = SwipeState(startState = startState)
    }

    /** Update swipe distance to [dy] */
    fun swipeTo(dy: Float) {
        // Only handle swipe up and stashed or collapsed bar
        if (dy > 0 || swipeState.expandedOnStart) return

        if (!canHandleSwipe(dy)) {
            return
        }
        animatedSwipeTranslation.updateValue(dy)

        val prevState = swipeState
@@ -90,38 +95,63 @@ class BubbleBarSwipeController {
        val passedUnstashThreshold = isUnstash(dy) || prevState.passedUnstashThreshold
        // Expand happens at the end of the gesture, always keep the current value
        val passedExpandThreshold = isExpand(dy)
        // Stash happens at the end of the gesture, always keep the current value
        val passedStashThreshold = isStash(dy)

        if (
            passedUnstashThreshold != prevState.passedUnstashThreshold ||
                passedExpandThreshold != prevState.passedExpandThreshold
                passedExpandThreshold != prevState.passedExpandThreshold ||
                passedStashThreshold != prevState.passedStashThreshold
        ) {
            swipeState =
                swipeState.copy(
                    passedUnstashThreshold = passedUnstashThreshold,
                    passedExpandThreshold = passedExpandThreshold,
                    passedStashThreshold = passedStashThreshold,
                )
        }

        if (
            swipeState.stashedOnStart &&
            swipeState.startState == STASHED &&
                swipeState.passedUnstashThreshold &&
                !prevState.passedUnstashThreshold
        ) {
            bubbleStashController?.showBubbleBar(expandBubbles = false)
            bubbleStashController.showBubbleBar(expandBubbles = false)
        }
    }

    /** Finish tracking swipe gesture. Animate views back to resting state */
    fun finish() {
        if (swipeState.passedExpandThreshold) {
            bubbleStashController?.showBubbleBar(expandBubbles = true)
        when {
            swipeState.passedExpandThreshold &&
                swipeState.startState in setOf(STASHED, COLLAPSED) -> {
                bubbleStashController.showBubbleBar(expandBubbles = true)
            }
            swipeState.passedStashThreshold && swipeState.startState == COLLAPSED -> {
                bubbleStashController.stashBubbleBar()
            }
        }
        if (animatedSwipeTranslation.value == 0f) {
            reset()
        } else {
            springToRest()
        }
    }

    /** Returns `true` if we are tracking a swipe gesture */
    fun isSwipeGesture(): Boolean {
        return swipeState.passedUnstashThreshold || swipeState.passedExpandThreshold
        return swipeState.passedUnstashThreshold ||
            swipeState.passedExpandThreshold ||
            swipeState.passedStashThreshold
    }

    private fun canHandleSwipe(dy: Float): Boolean {
        return when (swipeState.startState) {
            STASHED -> dy < 0 // stashed bar only handles swipe up
            COLLAPSED -> true // collapsed bar can be swiped in either direction
            UNKNOWN,
            EXPANDED -> false // expanded bar can't be swiped
        }
    }

    private fun isUnstash(dy: Float): Boolean {
@@ -132,6 +162,10 @@ class BubbleBarSwipeController {
        return dy < -expandThreshold
    }

    private fun isStash(dy: Float): Boolean {
        return dy > stashThreshold
    }

    private fun reset() {
        springAnimation?.let {
            if (it.isRunning) {
@@ -147,7 +181,7 @@ class BubbleBarSwipeController {
    private fun onSwipeUpdate(value: Float) {
        val dampedSwipe = -OverScroll.dampedScroll(-value, maxOverscroll).toFloat()
        bubbleStashedHandleViewController?.setTranslationYForSwipe(dampedSwipe)
        bubbleBarViewController?.setTranslationYForSwipe(dampedSwipe)
        bubbleBarViewController.setTranslationYForSwipe(dampedSwipe)
    }

    private fun springToRest() {
@@ -163,19 +197,26 @@ class BubbleBarSwipeController {
    }

    internal data class SwipeState(
        val stashedOnStart: Boolean = false,
        val collapsedOnStart: Boolean = false,
        val expandedOnStart: Boolean = false,
        val startState: StartState = UNKNOWN,
        val passedUnstashThreshold: Boolean = false,
        val passedExpandThreshold: Boolean = false,
        val passedStashThreshold: Boolean = false,
    )

    internal enum class StartState {
        UNKNOWN,
        STASHED,
        COLLAPSED,
        EXPANDED,
    }

    /** Allows overriding the dimension provider for testing */
    @VisibleForTesting
    interface DimensionProvider {
        val unstashThreshold: Int
        val expandThreshold: Int
        val maxOverscroll: Int
        val stashThreshold: Int
    }

    private class DefaultDimensionProvider(taskbarActivityContext: TaskbarActivityContext) :
@@ -183,6 +224,7 @@ class BubbleBarSwipeController {
        override val unstashThreshold: Int
        override val expandThreshold: Int
        override val maxOverscroll: Int
        override val stashThreshold: Int

        init {
            val resources = taskbarActivityContext.resources
@@ -198,6 +240,7 @@ class BubbleBarSwipeController {
                    taskbarActivityContext.deviceProfile,
                )
            maxOverscroll = taskbarActivityContext.deviceProfile.heightPx - unstashThreshold
            stashThreshold = resources.getDimensionPixelSize(R.dimen.taskbar_to_nav_threshold)
        }
    }
}
+99 −10
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
import com.android.launcher3.touch.OverScroll
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlin.math.abs
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -46,11 +47,13 @@ class BubbleBarSwipeControllerTest {
        const val UNSTASH_THRESHOLD = 100
        const val EXPAND_THRESHOLD = 200
        const val MAX_OVERSCROLL = 300
        const val STASH_THRESHOLD = 50

        const val UP_BELOW_UNSTASH = -UNSTASH_THRESHOLD + 10f
        const val UP_ABOVE_UNSTASH = -UNSTASH_THRESHOLD - 10f
        const val UP_ABOVE_EXPAND = -EXPAND_THRESHOLD - 10f
        const val DOWN_BELOW_UNSTASH = UNSTASH_THRESHOLD + 10f
        const val DOWN_UNDER_STASH = STASH_THRESHOLD - 10f
        const val DOWN_OVER_STASH = STASH_THRESHOLD + 10f
    }

    private val context = ApplicationProvider.getApplicationContext<Context>()
@@ -82,6 +85,9 @@ class BubbleBarSwipeControllerTest {

                override val maxOverscroll: Int
                    get() = MAX_OVERSCROLL

                override val stashThreshold: Int
                    get() = STASH_THRESHOLD
            }
        bubbleBarSwipeController = BubbleBarSwipeController(context, dimensionProvider)

@@ -102,8 +108,12 @@ class BubbleBarSwipeControllerTest {
        bubbleBarSwipeController.init(bubbleControllers)
    }

    // region Test that views have damped translation on swipe

    private fun testViewsHaveDampedTranslationOnSwipe(swipe: Float) {
        val dampedTranslation = -OverScroll.dampedScroll(-swipe, MAX_OVERSCROLL).toFloat()
        val isUp = swipe < 0
        val damped = OverScroll.dampedScroll(abs(swipe), MAX_OVERSCROLL).toFloat()
        val dampedTranslation = if (isUp) -damped else damped
        getInstrumentation().runOnMainSync {
            bubbleBarSwipeController.start()
            bubbleBarSwipeController.swipeTo(swipe)
@@ -142,6 +152,22 @@ class BubbleBarSwipeControllerTest {
        testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_EXPAND)
    }

    @Test
    fun swipeDown_collapsedBar_belowStashThreshold_viewsHaveDampedTranslation() {
        setUpCollapsedBar()
        testViewsHaveDampedTranslationOnSwipe(DOWN_UNDER_STASH)
    }

    @Test
    fun swipeDown_collapsedBar_overStashThreshold_viewsHaveDampedTranslation() {
        setUpCollapsedBar()
        testViewsHaveDampedTranslationOnSwipe(DOWN_OVER_STASH)
    }

    // endregion

    // region Test that translation on views is reset on finish

    private fun testViewsTranslationResetOnFinish(swipe: Float) {
        getInstrumentation().runOnMainSync {
            bubbleBarSwipeController.start()
@@ -194,6 +220,16 @@ class BubbleBarSwipeControllerTest {
        testViewsTranslationResetOnFinish(UP_ABOVE_EXPAND)
    }

    @Test
    fun swipeDown_collapsedBar_aboveStashThreshold_animateTranslationToZeroOnFinish() {
        setUpCollapsedBar()
        testViewsTranslationResetOnFinish(DOWN_OVER_STASH)
    }

    // endregion

    // region Test swipe interactions on stashed bar

    @Test
    fun swipeUp_stashedBar_belowUnstashThreshold_doesNotShowBar() {
        setUpStashedBar()
@@ -282,31 +318,84 @@ class BubbleBarSwipeControllerTest {
    }

    @Test
    fun swipeUp_expandedBar_swipeIgnored() {
        setUpExpandedBar()
    fun swipeDown_stashedBar_swipeIgnored() {
        setUpStashedBar()
        getInstrumentation().runOnMainSync {
            bubbleBarSwipeController.start()
            bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
            bubbleBarSwipeController.swipeTo(DOWN_BELOW_UNSTASH)
            bubbleBarSwipeController.finish()
            bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH)
        }
        verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any())
        verify(bubbleBarViewController, never()).setTranslationYForSwipe(any())
        verify(bubbleStashController, never()).showBubbleBar(any())
    }

    // endregion

    // region Test swipe interactions on expanded bar

    @Test
    fun swipeDown_stashedBar_swipeIgnored() {
        setUpStashedBar()
    fun swipe_expandedBar_swipeIgnored() {
        setUpExpandedBar()
        getInstrumentation().runOnMainSync {
            bubbleBarSwipeController.start()
            bubbleBarSwipeController.swipeTo(DOWN_BELOW_UNSTASH)
            bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
            bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH)
            bubbleBarSwipeController.finish()
        }
        verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any())
        verify(bubbleBarViewController, never()).setTranslationYForSwipe(any())
        verify(bubbleStashController, never()).showBubbleBar(any())
    }

    // endregion

    // region Test swipe interactions on collapsed bar

    @Test
    fun swipeDown_collapsedBar_underStashThreshold_doesNotHideBar() {
        setUpCollapsedBar()
        getInstrumentation().runOnMainSync {
            bubbleBarSwipeController.start()
            bubbleBarSwipeController.swipeTo(DOWN_UNDER_STASH)
            bubbleBarSwipeController.finish()
        }
        verify(bubbleStashController, never()).stashBubbleBar()
    }

    @Test
    fun swipeDown_collapsedBar_overStashThreshold_doesNotHideBarBeforeFinish() {
        setUpCollapsedBar()
        getInstrumentation().runOnMainSync {
            bubbleBarSwipeController.start()
            bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH)
        }
        verify(bubbleStashController, never()).stashBubbleBar()
        getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() }
        verify(bubbleStashController).stashBubbleBar()
    }

    @Test
    fun swipeDown_collapsedBar_underStashThreshold_isSwipeGestureFalse() {
        setUpCollapsedBar()
        getInstrumentation().runOnMainSync {
            bubbleBarSwipeController.start()
            bubbleBarSwipeController.swipeTo(DOWN_UNDER_STASH)
        }
        assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse()
    }

    @Test
    fun swipeDown_collapsedBar_overStashThreshold_isSwipeGestureTrue() {
        setUpCollapsedBar()
        getInstrumentation().runOnMainSync {
            bubbleBarSwipeController.start()
            bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH)
        }
        assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue()
    }

    // endregion

    private fun setUpStashedBar() {
        whenever(bubbleStashController.isStashed).thenReturn(true)
        whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(false)