summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Omar Miatello <omarmt@google.com> 2023-11-14 08:39:05 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2023-11-14 08:39:05 +0000
commitae0802742c0ae889174462c349a96ba67cfe2354 (patch)
tree963d56a5e81dfd12f127af9c12322ca33454ddf4
parentdc8cf702d05b77b52c8f397bf1ec25531a5bbd77 (diff)
parent6384061e0c9346fcf42269b0da0874c718ba40f8 (diff)
Merge changes from topic "transitionInterceptionThreshold" into main
* changes: Add TestGestureScope.progress to make SceneGestureHandlerTest more readable The animation between scenes can only be intercepted in a defined range
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt32
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt90
4 files changed, 109 insertions, 21 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 9d71801be25b..838cb3bd5fba 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -39,13 +39,13 @@ import kotlinx.coroutines.launch
@VisibleForTesting
class SceneGestureHandler(
- private val layoutImpl: SceneTransitionLayoutImpl,
+ internal val layoutImpl: SceneTransitionLayoutImpl,
internal val orientation: Orientation,
private val coroutineScope: CoroutineScope,
) {
val draggable: DraggableHandler = SceneDraggableHandler(this)
- private var transitionState
+ internal var transitionState
get() = layoutImpl.state.transitionState
set(value) {
layoutImpl.state.transitionState = value
@@ -58,7 +58,7 @@ class SceneGestureHandler(
* Note: the initialScene here does not matter, it's only used for initializing the transition
* and will be replaced when a drag event starts.
*/
- private val swipeTransition = SwipeTransition(initialScene = currentScene)
+ internal val swipeTransition = SwipeTransition(initialScene = currentScene)
internal val currentScene: Scene
get() = layoutImpl.scene(transitionState.currentScene)
@@ -415,7 +415,7 @@ class SceneGestureHandler(
}
}
- private class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
+ internal class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
var _currentScene by mutableStateOf(initialScene)
override val currentScene: SceneKey
get() = _currentScene.key
@@ -598,9 +598,29 @@ class SceneNestedScrollHandler(
return PriorityNestedScrollConnection(
canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
canChangeScene = offsetBeforeStart == Offset.Zero
- gestureHandler.isDrivingTransition &&
+
+ val canInterceptSwipeTransition =
canChangeScene &&
- offsetAvailable.toAmount() != 0f
+ gestureHandler.isDrivingTransition &&
+ offsetAvailable.toAmount() != 0f
+ if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false
+
+ val progress = gestureHandler.swipeTransition.progress
+ val threshold = gestureHandler.layoutImpl.transitionInterceptionThreshold
+ fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold
+
+ // The transition is always between 0 and 1. If it is close to either of these
+ // intervals, we want to go directly to the TransitionState.Idle.
+ // The progress value can go beyond this range in the case of overscroll.
+ val shouldSnapToIdle = isProgressCloseTo(0f) || isProgressCloseTo(1f)
+ if (shouldSnapToIdle) {
+ gestureHandler.swipeTransition.stopOffsetAnimation()
+ gestureHandler.transitionState =
+ TransitionState.Idle(gestureHandler.swipeTransition.currentScene)
+ }
+
+ // Start only if we cannot consume this event
+ !shouldSnapToIdle
},
canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
val amount = offsetAvailable.toAmount()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index efdfe7a7921e..9c31445e1b96 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import androidx.annotation.FloatRange
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -41,6 +42,8 @@ import androidx.compose.ui.platform.LocalDensity
* @param transitions the definition of the transitions used to animate a change of scene.
* @param state the observable state of this layout.
* @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
+ * @param transitionInterceptionThreshold used during a scene transition. For the scene to be
+ * intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param scenes the configuration of the different scenes of this layout.
*/
@Composable
@@ -51,6 +54,7 @@ fun SceneTransitionLayout(
modifier: Modifier = Modifier,
state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
edgeDetector: EdgeDetector = DefaultEdgeDetector,
+ @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
val density = LocalDensity.current
@@ -63,6 +67,7 @@ fun SceneTransitionLayout(
state = state,
density = density,
edgeDetector = edgeDetector,
+ transitionInterceptionThreshold = transitionInterceptionThreshold,
coroutineScope = coroutineScope,
)
}
@@ -71,6 +76,7 @@ fun SceneTransitionLayout(
layoutImpl.transitions = transitions
layoutImpl.density = density
layoutImpl.edgeDetector = edgeDetector
+ layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
layoutImpl.setScenes(scenes)
layoutImpl.setCurrentScene(currentScene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 0b06953bc8e2..94f2737039f4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -50,6 +50,7 @@ class SceneTransitionLayoutImpl(
internal val state: SceneTransitionLayoutState,
density: Density,
edgeDetector: EdgeDetector,
+ transitionInterceptionThreshold: Float,
coroutineScope: CoroutineScope,
) {
internal val scenes = SnapshotStateMap<SceneKey, Scene>()
@@ -62,6 +63,7 @@ class SceneTransitionLayoutImpl(
internal var transitions by mutableStateOf(transitions)
internal var density: Density by mutableStateOf(density)
internal var edgeDetector by mutableStateOf(edgeDetector)
+ internal var transitionInterceptionThreshold by mutableStateOf(transitionInterceptionThreshold)
private val horizontalGestureHandler: SceneGestureHandler
private val verticalGestureHandler: SceneGestureHandler
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 7ab2096b3d88..49ef31b16d73 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -69,6 +69,8 @@ class SceneGestureHandlerTest {
scene(SceneC) { Text("SceneC") }
}
+ val transitionInterceptionThreshold = 0.05f
+
val sceneGestureHandler =
SceneGestureHandler(
layoutImpl =
@@ -79,6 +81,7 @@ class SceneGestureHandlerTest {
state = layoutState,
density = Density(1f),
edgeDetector = DefaultEdgeDetector,
+ transitionInterceptionThreshold = transitionInterceptionThreshold,
coroutineScope = coroutineScope,
)
.apply { setScenesTargetSizeForTest(LAYOUT_SIZE) },
@@ -107,6 +110,9 @@ class SceneGestureHandlerTest {
val transitionState: TransitionState
get() = layoutState.transitionState
+ val progress: Float
+ get() = (transitionState as Transition).progress
+
fun advanceUntilIdle() {
coroutineScope.testScheduler.advanceUntilIdle()
}
@@ -145,13 +151,12 @@ class SceneGestureHandlerTest {
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
draggable.onDragStarted()
assertScene(currentScene = SceneA, isIdle = false)
- val transition = transitionState as Transition
draggable.onDelta(pixels = deltaInPixels10)
- assertThat(transition.progress).isEqualTo(0.1f)
+ assertThat(progress).isEqualTo(0.1f)
draggable.onDelta(pixels = deltaInPixels10)
- assertThat(transition.progress).isEqualTo(0.2f)
+ assertThat(progress).isEqualTo(0.2f)
}
@Test
@@ -257,8 +262,7 @@ class SceneGestureHandlerTest {
)
assertScene(currentScene = SceneA, isIdle = false)
- val transition = transitionState as Transition
- assertThat(transition.progress).isEqualTo(0.1f)
+ assertThat(progress).isEqualTo(0.1f)
assertThat(consumed).isEqualTo(offsetY10)
}
@@ -282,13 +286,12 @@ class SceneGestureHandlerTest {
nestedScroll.scroll(available = offsetY10)
assertScene(currentScene = SceneA, isIdle = false)
- val transition = transitionState as Transition
- assertThat(transition.progress).isEqualTo(0.1f)
+ assertThat(progress).isEqualTo(0.1f)
// start intercept preScroll
val consumed =
nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag)
- assertThat(transition.progress).isEqualTo(0.2f)
+ assertThat(progress).isEqualTo(0.2f)
// do nothing on postScroll
nestedScroll.onPostScroll(
@@ -296,13 +299,71 @@ class SceneGestureHandlerTest {
available = Offset.Zero,
source = NestedScrollSource.Drag
)
- assertThat(transition.progress).isEqualTo(0.2f)
+ assertThat(progress).isEqualTo(0.2f)
nestedScroll.scroll(available = offsetY10)
- assertThat(transition.progress).isEqualTo(0.3f)
+ assertThat(progress).isEqualTo(0.3f)
assertScene(currentScene = SceneA, isIdle = false)
}
+ private suspend fun TestGestureScope.preScrollAfterSceneTransition(
+ firstScroll: Float,
+ secondScroll: Float
+ ) {
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
+ // start scene transition
+ nestedScroll.scroll(available = Offset(0f, SCREEN_SIZE * firstScroll))
+
+ // stop scene transition (start the "stop animation")
+ nestedScroll.onPreFling(available = Velocity.Zero)
+
+ // a pre scroll event, that could be intercepted by SceneGestureHandler
+ nestedScroll.onPreScroll(Offset(0f, SCREEN_SIZE * secondScroll), NestedScrollSource.Drag)
+ }
+
+ // Float tolerance for comparisons
+ private val tolerance = 0.00001f
+
+ @Test
+ fun scrollAndFling_scrollLessThanInterceptable_goToIdleOnCurrentScene() = runGestureTest {
+ val first = transitionInterceptionThreshold - tolerance
+ val second = 0.01f
+
+ preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+
+ assertScene(SceneA, isIdle = true)
+ }
+
+ @Test
+ fun scrollAndFling_scrollMinInterceptable_interceptPreScrollEvents() = runGestureTest {
+ val first = transitionInterceptionThreshold + tolerance
+ val second = 0.01f
+
+ preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+
+ assertThat(progress).isWithin(tolerance).of(first + second)
+ }
+
+ @Test
+ fun scrollAndFling_scrollMaxInterceptable_interceptPreScrollEvents() = runGestureTest {
+ val first = 1f - transitionInterceptionThreshold - tolerance
+ val second = 0.01f
+
+ preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+
+ assertThat(progress).isWithin(tolerance).of(first + second)
+ }
+
+ @Test
+ fun scrollAndFling_scrollMoreThanInterceptable_goToIdleOnNextScene() = runGestureTest {
+ val first = 1f - transitionInterceptionThreshold + tolerance
+ val second = 0.01f
+
+ preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+
+ assertScene(SceneC, isIdle = true)
+ }
+
@Test
fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
@@ -444,24 +505,23 @@ class SceneGestureHandlerTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always)
draggable.onDragStarted()
assertScene(currentScene = SceneA, isIdle = false)
- val transition = transitionState as Transition
draggable.onDelta(deltaInPixels10)
- assertThat(transition.progress).isEqualTo(0.1f)
+ assertThat(progress).isEqualTo(0.1f)
// now we can intercept the scroll events
nestedScroll.scroll(available = offsetY10)
- assertThat(transition.progress).isEqualTo(0.2f)
+ assertThat(progress).isEqualTo(0.2f)
// this should be ignored, we are scrolling now!
draggable.onDragStopped(velocityThreshold)
assertScene(currentScene = SceneA, isIdle = false)
nestedScroll.scroll(available = offsetY10)
- assertThat(transition.progress).isEqualTo(0.3f)
+ assertThat(progress).isEqualTo(0.3f)
nestedScroll.scroll(available = offsetY10)
- assertThat(transition.progress).isEqualTo(0.4f)
+ assertThat(progress).isEqualTo(0.4f)
nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
assertScene(currentScene = SceneC, isIdle = false)