Merge "Have bubble bar unstash gesture track finger" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 084067c..a59445b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -108,6 +108,7 @@
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
import com.android.launcher3.taskbar.bubbles.BubbleBarController;
import com.android.launcher3.taskbar.bubbles.BubbleBarPinController;
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController;
import com.android.launcher3.taskbar.bubbles.BubbleBarView;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
@@ -279,9 +280,11 @@
BubbleBarController.onTaskbarRecreated();
if (BubbleBarController.isBubbleBarEnabled() && bubbleBarView != null) {
Optional<BubbleStashedHandleViewController> bubbleHandleController = Optional.empty();
+ Optional<BubbleBarSwipeController> bubbleBarSwipeController = Optional.empty();
if (isTransientTaskbar) {
bubbleHandleController = Optional.of(
new BubbleStashedHandleViewController(this, bubbleHandleView));
+ bubbleBarSwipeController = Optional.of(new BubbleBarSwipeController(this));
}
TaskbarHotseatDimensionsProvider dimensionsProvider =
new DeviceProfileDimensionsProviderAdapter(this);
@@ -299,6 +302,7 @@
() -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
new BubblePinController(this, mDragLayer,
() -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
+ bubbleBarSwipeController,
new BubbleCreator(this)
));
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt
new file mode 100644
index 0000000..a831fd7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles
+
+import android.animation.ValueAnimator
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.doOnEnd
+import androidx.dynamicanimation.animation.SpringForce
+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.stashing.BubbleStashController
+import com.android.launcher3.touch.OverScroll
+
+/** Handle swipe events on the bubble bar and handle */
+class BubbleBarSwipeController {
+
+ private val context: Context
+
+ private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null
+ private var bubbleBarViewController: BubbleBarViewController? = null
+ private var bubbleStashController: BubbleStashController? = null
+
+ private var springAnimation: ValueAnimator? = null
+ private val animatedSwipeTranslation = AnimatedFloat(this::onSwipeUpdate)
+
+ private val unstashThreshold: Int
+ private val expandThreshold: Int
+ private val maxOverscroll: Int
+
+ private var swipeState: SwipeState = SwipeState()
+
+ constructor(tac: TaskbarActivityContext) : this(tac, DefaultDimensionProvider(tac))
+
+ @VisibleForTesting
+ constructor(context: Context, dimensionProvider: DimensionProvider) {
+ this.context = context
+ unstashThreshold = dimensionProvider.unstashThreshold
+ expandThreshold = dimensionProvider.expandThreshold
+ maxOverscroll = dimensionProvider.maxOverscroll
+ }
+
+ fun init(bubbleControllers: BubbleControllers) {
+ bubbleStashedHandleViewController =
+ bubbleControllers.bubbleStashedHandleViewController.orElse(null)
+ bubbleBarViewController = bubbleControllers.bubbleBarViewController
+ bubbleStashController = bubbleControllers.bubbleStashController
+ }
+
+ /** 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,
+ )
+ }
+
+ /** Update swipe distance to [dy] */
+ fun swipeTo(dy: Float) {
+ // Only handle swipe up and stashed or collapsed bar
+ if (dy > 0 || swipeState.expandedOnStart) return
+
+ animatedSwipeTranslation.updateValue(dy)
+
+ val prevState = swipeState
+ // We can pass unstash threshold once per gesture, keep it true if it happened once
+ val passedUnstashThreshold = isUnstash(dy) || prevState.passedUnstashThreshold
+ // Expand happens at the end of the gesture, always keep the current value
+ val passedExpandThreshold = isExpand(dy)
+
+ if (
+ passedUnstashThreshold != prevState.passedUnstashThreshold ||
+ passedExpandThreshold != prevState.passedExpandThreshold
+ ) {
+ swipeState =
+ swipeState.copy(
+ passedUnstashThreshold = passedUnstashThreshold,
+ passedExpandThreshold = passedExpandThreshold,
+ )
+ }
+
+ if (
+ swipeState.stashedOnStart &&
+ swipeState.passedUnstashThreshold &&
+ !prevState.passedUnstashThreshold
+ ) {
+ bubbleStashController?.showBubbleBar(expandBubbles = false)
+ }
+ }
+
+ /** Finish tracking swipe gesture. Animate views back to resting state */
+ fun finish() {
+ if (swipeState.passedExpandThreshold) {
+ bubbleStashController?.showBubbleBar(expandBubbles = true)
+ }
+ springToRest()
+ }
+
+ /** Returns `true` if we are tracking a swipe gesture */
+ fun isSwipeGesture(): Boolean {
+ return swipeState.passedUnstashThreshold || swipeState.passedExpandThreshold
+ }
+
+ private fun isUnstash(dy: Float): Boolean {
+ return dy < -unstashThreshold
+ }
+
+ private fun isExpand(dy: Float): Boolean {
+ return dy < -expandThreshold
+ }
+
+ private fun reset() {
+ springAnimation?.let {
+ if (it.isRunning) {
+ it.removeAllListeners()
+ it.cancel()
+ animatedSwipeTranslation.updateValue(0f)
+ }
+ }
+ springAnimation = null
+ swipeState = SwipeState()
+ }
+
+ private fun onSwipeUpdate(value: Float) {
+ val dampedSwipe = -OverScroll.dampedScroll(-value, maxOverscroll).toFloat()
+ bubbleStashedHandleViewController?.setTranslationYForSwipe(dampedSwipe)
+ bubbleBarViewController?.setTranslationYForSwipe(dampedSwipe)
+ }
+
+ private fun springToRest() {
+ springAnimation =
+ SpringAnimationBuilder(context)
+ .setStartValue(animatedSwipeTranslation.value)
+ .setEndValue(0f)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+ .setStiffness(SpringForce.STIFFNESS_LOW)
+ .build(animatedSwipeTranslation, AnimatedFloat.VALUE)
+ .also { it.doOnEnd { reset() } }
+ springAnimation?.start()
+ }
+
+ internal data class SwipeState(
+ val stashedOnStart: Boolean = false,
+ val collapsedOnStart: Boolean = false,
+ val expandedOnStart: Boolean = false,
+ val passedUnstashThreshold: Boolean = false,
+ val passedExpandThreshold: Boolean = false,
+ )
+
+ /** Allows overriding the dimension provider for testing */
+ @VisibleForTesting
+ interface DimensionProvider {
+ val unstashThreshold: Int
+ val expandThreshold: Int
+ val maxOverscroll: Int
+ }
+
+ private class DefaultDimensionProvider(taskbarActivityContext: TaskbarActivityContext) :
+ DimensionProvider {
+ override val unstashThreshold: Int
+ override val expandThreshold: Int
+ override val maxOverscroll: Int
+
+ init {
+ val resources = taskbarActivityContext.resources
+ unstashThreshold =
+ TaskbarThresholdUtils.getFromNavThreshold(
+ resources,
+ taskbarActivityContext.deviceProfile,
+ )
+ // TODO(325673340): review threshold with ux
+ expandThreshold =
+ TaskbarThresholdUtils.getAppWindowThreshold(
+ resources,
+ taskbarActivityContext.deviceProfile,
+ )
+ maxOverscroll = taskbarActivityContext.deviceProfile.heightPx - unstashThreshold
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index a66df4c..8230f42 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -40,6 +40,7 @@
public final BubbleDismissController bubbleDismissController;
public final BubbleBarPinController bubbleBarPinController;
public final BubblePinController bubblePinController;
+ public final Optional<BubbleBarSwipeController> bubbleBarSwipeController;
public final BubbleCreator bubbleCreator;
private final RunnableList mPostInitRunnables = new RunnableList();
@@ -58,6 +59,7 @@
BubbleDismissController bubbleDismissController,
BubbleBarPinController bubbleBarPinController,
BubblePinController bubblePinController,
+ Optional<BubbleBarSwipeController> bubbleBarSwipeController,
BubbleCreator bubbleCreator) {
this.bubbleBarController = bubbleBarController;
this.bubbleBarViewController = bubbleBarViewController;
@@ -67,6 +69,7 @@
this.bubbleDismissController = bubbleDismissController;
this.bubbleBarPinController = bubbleBarPinController;
this.bubblePinController = bubblePinController;
+ this.bubbleBarSwipeController = bubbleBarSwipeController;
this.bubbleCreator = bubbleCreator;
}
@@ -111,6 +114,7 @@
bubbleDismissController.init(/* bubbleControllers = */ this);
bubbleBarPinController.init(this, bubbleBarLocationListeners);
bubblePinController.init(this);
+ bubbleBarSwipeController.ifPresent(c -> c.init(this));
mPostInitRunnables.executeAllAndDestroy();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
index 12b1487..340a120 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
@@ -44,8 +44,6 @@
import android.view.LayoutInflater;
import android.view.ViewGroup;
-import androidx.appcompat.content.res.AppCompatResources;
-
import com.android.internal.graphics.ColorUtils;
import com.android.launcher3.R;
import com.android.launcher3.icons.BitmapInfo;
@@ -196,8 +194,7 @@
}
private Bitmap createOverflowBitmap() {
- Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
- R.drawable.bubble_ic_overflow_button);
+ Drawable iconDrawable = mContext.getDrawable(R.drawable.bubble_ic_overflow_button);
final TypedArray ta = mContext.obtainStyledAttributes(
new int[]{
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index 92031c5..778c231 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -23,10 +23,12 @@
import android.view.MotionEvent;
import android.view.ViewConfiguration;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
-import com.android.launcher3.taskbar.bubbles.BubbleDragController;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -40,10 +42,11 @@
private final BubbleStashController mBubbleStashController;
private final BubbleBarViewController mBubbleBarViewController;
- private final BubbleDragController mBubbleDragController;
+ @Nullable
+ private final BubbleBarSwipeController mBubbleBarSwipeController;
private final InputMonitorCompat mInputMonitorCompat;
- private boolean mSwipeUpOnBubbleHandle;
+ private boolean mPilfered;
private boolean mPassedTouchSlop;
private boolean mStashedOrCollapsedOnDown;
@@ -57,7 +60,8 @@
InputMonitorCompat inputMonitorCompat) {
mBubbleStashController = bubbleControllers.bubbleStashController;
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
- mBubbleDragController = bubbleControllers.bubbleDragController;
+ mBubbleBarSwipeController = bubbleControllers.bubbleBarSwipeController.orElse(null);
+
mInputMonitorCompat = inputMonitorCompat;
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mTimeForTap = ViewConfiguration.getTapTimeout();
@@ -77,6 +81,9 @@
mDownPos.set(ev.getX(), ev.getY());
mLastPos.set(mDownPos);
mStashedOrCollapsedOnDown = mBubbleStashController.isStashed() || isCollapsed();
+ if (mBubbleBarSwipeController != null) {
+ mBubbleBarSwipeController.start();
+ }
break;
case MotionEvent.ACTION_MOVE:
int pointerIndex = ev.findPointerIndex(mActivePointerId);
@@ -90,11 +97,10 @@
if (!mPassedTouchSlop) {
mPassedTouchSlop = Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop;
}
- if (mStashedOrCollapsedOnDown && !mSwipeUpOnBubbleHandle && mPassedTouchSlop) {
- boolean verticalGesture = Math.abs(dY) > Math.abs(dX);
- if (verticalGesture && !mBubbleDragController.isDragging()) {
- mSwipeUpOnBubbleHandle = true;
- mBubbleStashController.showBubbleBar(/* expandBubbles= */ true);
+ if (mBubbleBarSwipeController != null) {
+ mBubbleBarSwipeController.swipeTo(dY);
+ if (!mPilfered && mBubbleBarSwipeController.isSwipeGesture()) {
+ mPilfered = true;
// Bubbles is handling the swipe so make sure no one else gets it.
TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
mInputMonitorCompat.pilferPointers();
@@ -102,8 +108,10 @@
}
break;
case MotionEvent.ACTION_UP:
+ boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null
+ && mBubbleBarSwipeController.isSwipeGesture();
boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForTap;
- if (isWithinTapTime && !mSwipeUpOnBubbleHandle && !mPassedTouchSlop
+ if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
&& mStashedOrCollapsedOnDown) {
// Taps on the handle / collapsed state should open the bar
mBubbleStashController.showBubbleBar(/* expandBubbles= */ true);
@@ -116,8 +124,11 @@
}
private void cleanupAfterMotionEvent() {
+ if (mBubbleBarSwipeController != null) {
+ mBubbleBarSwipeController.finish();
+ }
mPassedTouchSlop = false;
- mSwipeUpOnBubbleHandle = false;
+ mPilfered = false;
}
private boolean isCollapsed() {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
index 785ec66..c8f50f7 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
@@ -49,6 +49,7 @@
@Mock private lateinit var bubbleDismissController: BubbleDismissController
@Mock private lateinit var bubbleBarPinController: BubbleBarPinController
@Mock private lateinit var bubblePinController: BubblePinController
+ @Mock private lateinit var bubbleBarSwipeController: BubbleBarSwipeController
@Mock private lateinit var bubbleCreator: BubbleCreator
@Mock private lateinit var motionEvent: MotionEvent
@@ -67,7 +68,8 @@
bubbleDismissController,
bubbleBarPinController,
bubblePinController,
- bubbleCreator
+ Optional.of(bubbleBarSwipeController),
+ bubbleCreator,
)
}
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
new file mode 100644
index 0000000..97847be
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles
+
+import android.animation.AnimatorTestRule
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+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 org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class BubbleBarSwipeControllerTest {
+
+ companion object {
+ const val UNSTASH_THRESHOLD = 100
+ const val EXPAND_THRESHOLD = 200
+ const val MAX_OVERSCROLL = 300
+
+ 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
+ }
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+
+ @get:Rule(order = 0) val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ @get:Rule(order = 1) val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
+
+ private lateinit var bubbleBarSwipeController: BubbleBarSwipeController
+
+ @Mock private lateinit var bubbleBarController: BubbleBarController
+ @Mock private lateinit var bubbleBarViewController: BubbleBarViewController
+ @Mock private lateinit var bubbleStashController: BubbleStashController
+ @Mock private lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController
+ @Mock private lateinit var bubbleDragController: BubbleDragController
+ @Mock private lateinit var bubbleDismissController: BubbleDismissController
+ @Mock private lateinit var bubbleBarPinController: BubbleBarPinController
+ @Mock private lateinit var bubblePinController: BubblePinController
+ @Mock private lateinit var bubbleCreator: BubbleCreator
+
+ @Before
+ fun setUp() {
+ val dimensionProvider =
+ object : BubbleBarSwipeController.DimensionProvider {
+ override val unstashThreshold: Int
+ get() = UNSTASH_THRESHOLD
+
+ override val expandThreshold: Int
+ get() = EXPAND_THRESHOLD
+
+ override val maxOverscroll: Int
+ get() = MAX_OVERSCROLL
+ }
+ bubbleBarSwipeController = BubbleBarSwipeController(context, dimensionProvider)
+
+ val bubbleControllers =
+ BubbleControllers(
+ bubbleBarController,
+ bubbleBarViewController,
+ bubbleStashController,
+ Optional.of(bubbleStashedHandleViewController),
+ bubbleDragController,
+ bubbleDismissController,
+ bubbleBarPinController,
+ bubblePinController,
+ Optional.of(bubbleBarSwipeController),
+ bubbleCreator,
+ )
+
+ bubbleBarSwipeController.init(bubbleControllers)
+ }
+
+ private fun testViewsHaveDampedTranslationOnSwipe(swipe: Float) {
+ val dampedTranslation = -OverScroll.dampedScroll(-swipe, MAX_OVERSCROLL).toFloat()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(swipe)
+ }
+ verify(bubbleStashedHandleViewController).setTranslationYForSwipe(dampedTranslation)
+ verify(bubbleBarViewController).setTranslationYForSwipe(dampedTranslation)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_belowUnstashThreshold_viewsHaveDampedTranslation() {
+ setUpStashedBar()
+ testViewsHaveDampedTranslationOnSwipe(UP_BELOW_UNSTASH)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() {
+ setUpStashedBar()
+ testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_aboveExpandThreshold_viewsHaveDampedTranslation() {
+ setUpStashedBar()
+ testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_EXPAND)
+ }
+
+ @Test
+ fun swipeUp_collapsedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() {
+ setUpCollapsedBar()
+ testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH)
+ }
+
+ @Test
+ fun swipeUp_collapsedBar_aboveExpandThreshold_viewsHaveDampedTranslation() {
+ setUpCollapsedBar()
+ testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_EXPAND)
+ }
+
+ private fun testViewsTranslationResetOnFinish(swipe: Float) {
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(swipe)
+ bubbleBarSwipeController.finish()
+ // We use a spring animation. Advance by 5 seconds to give it time to finish
+ animatorTestRule.advanceTimeBy(5000)
+ }
+ val handleSwipeTranslation = argumentCaptor<Float>()
+ val barSwipeTranslation = argumentCaptor<Float>()
+ verify(bubbleStashedHandleViewController, atLeastOnce())
+ .setTranslationYForSwipe(handleSwipeTranslation.capture())
+ verify(bubbleBarViewController, atLeastOnce())
+ .setTranslationYForSwipe(barSwipeTranslation.capture())
+
+ assertThat(handleSwipeTranslation.firstValue).isNonZero()
+ assertThat(handleSwipeTranslation.lastValue).isZero()
+
+ assertThat(barSwipeTranslation.firstValue).isNonZero()
+ assertThat(barSwipeTranslation.lastValue).isZero()
+ }
+
+ @Test
+ fun swipeUp_stashedBar_belowUnstashThreshold_animateTranslationToZeroOnFinish() {
+ setUpStashedBar()
+ testViewsTranslationResetOnFinish(UP_BELOW_UNSTASH)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() {
+ setUpStashedBar()
+ testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_aboveExpandThreshold_animateTranslationToZeroOnFinish() {
+ setUpStashedBar()
+ testViewsTranslationResetOnFinish(UP_ABOVE_EXPAND)
+ }
+
+ @Test
+ fun swipeUp_collapsedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() {
+ setUpCollapsedBar()
+ testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH)
+ }
+
+ @Test
+ fun swipeUp_collapsedBar_aboveExpandThreshold_animateTranslationToZeroOnFinish() {
+ setUpCollapsedBar()
+ testViewsTranslationResetOnFinish(UP_ABOVE_EXPAND)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_belowUnstashThreshold_doesNotShowBar() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+ }
+ verify(bubbleStashController, never()).showBubbleBar(any())
+ }
+
+ @Test
+ fun swipeUp_stashedBar_belowUnstashThreshold_isSwipeGestureFalse() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+ }
+ assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse()
+ }
+
+ @Test
+ fun swipeUp_stashedBar_aboveUnstashThreshold_unstashBubbleBar() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+ }
+ verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_overUnstashThreshold_isSwipeGestureTrue() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+ }
+ assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue()
+ }
+
+ @Test
+ fun swipeUp_stashedBar_overUnstashThresholdMultipleTimes_unstashBubbleBarOnce() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+ bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+ }
+ verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_overExpandThreshold_doesNotExpandBeforeFinish() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
+ }
+ verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+ getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() }
+ verify(bubbleStashController).showBubbleBar(expandBubbles = true)
+ }
+
+ @Test
+ fun swipeUp_stashedBar_overExpandThreshold_isSwipeGestureTrue() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
+ }
+ assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue()
+ }
+
+ @Test
+ fun swipeUp_stashedBar_overExpandThresholdAndBackDown_doesNotExpandAfterFinish() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+ }
+ verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+ getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() }
+ verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+ }
+
+ @Test
+ fun swipeUp_expandedBar_swipeIgnored() {
+ setUpExpandedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND)
+ bubbleBarSwipeController.swipeTo(DOWN_BELOW_UNSTASH)
+ bubbleBarSwipeController.finish()
+ }
+ verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any())
+ verify(bubbleBarViewController, never()).setTranslationYForSwipe(any())
+ verify(bubbleStashController, never()).showBubbleBar(any())
+ }
+
+ @Test
+ fun swipeDown_stashedBar_swipeIgnored() {
+ setUpStashedBar()
+ getInstrumentation().runOnMainSync {
+ bubbleBarSwipeController.start()
+ bubbleBarSwipeController.swipeTo(DOWN_BELOW_UNSTASH)
+ }
+ verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any())
+ verify(bubbleBarViewController, never()).setTranslationYForSwipe(any())
+ verify(bubbleStashController, never()).showBubbleBar(any())
+ }
+
+ private fun setUpStashedBar() {
+ whenever(bubbleStashController.isStashed).thenReturn(true)
+ whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(false)
+ whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+ }
+
+ private fun setUpCollapsedBar() {
+ whenever(bubbleStashController.isStashed).thenReturn(false)
+ whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true)
+ whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+ }
+
+ private fun setUpExpandedBar() {
+ whenever(bubbleStashController.isStashed).thenReturn(false)
+ whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true)
+ whenever(bubbleBarViewController.isExpanded).thenReturn(true)
+ }
+}