diff options
| author | 2023-11-14 08:39:05 +0000 | |
|---|---|---|
| committer | 2023-11-14 08:39:05 +0000 | |
| commit | ae0802742c0ae889174462c349a96ba67cfe2354 (patch) | |
| tree | 963d56a5e81dfd12f127af9c12322ca33454ddf4 | |
| parent | dc8cf702d05b77b52c8f397bf1ec25531a5bbd77 (diff) | |
| parent | 6384061e0c9346fcf42269b0da0874c718ba40f8 (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
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) |