diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt index a831fd7282..bc562a6039 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt @@ -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() } - 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) } } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt index 97847befaf..3b6952de25 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt @@ -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() @@ -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,12 +318,28 @@ class BubbleBarSwipeControllerTest { } @Test - fun swipeUp_expandedBar_swipeIgnored() { + fun swipeDown_stashedBar_swipeIgnored() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + 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 swipe_expandedBar_swipeIgnored() { setUpExpandedBar() getInstrumentation().runOnMainSync { bubbleBarSwipeController.start() bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND) - bubbleBarSwipeController.swipeTo(DOWN_BELOW_UNSTASH) + bubbleBarSwipeController.swipeTo(DOWN_OVER_STASH) bubbleBarSwipeController.finish() } verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) @@ -295,18 +347,55 @@ class BubbleBarSwipeControllerTest { verify(bubbleStashController, never()).showBubbleBar(any()) } + // endregion + + // region Test swipe interactions on collapsed bar + @Test - fun swipeDown_stashedBar_swipeIgnored() { - setUpStashedBar() + fun swipeDown_collapsedBar_underStashThreshold_doesNotHideBar() { + setUpCollapsedBar() getInstrumentation().runOnMainSync { bubbleBarSwipeController.start() - bubbleBarSwipeController.swipeTo(DOWN_BELOW_UNSTASH) + bubbleBarSwipeController.swipeTo(DOWN_UNDER_STASH) + bubbleBarSwipeController.finish() } - verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) - verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) - verify(bubbleStashController, never()).showBubbleBar(any()) + 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)