Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +10 −18 Original line number Diff line number Diff line Loading @@ -701,27 +701,19 @@ internal class SceneNestedScrollHandler( gestureHandler.shouldImmediatelyIntercept(startedPosition = null) if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false val swipeTransition = gestureHandler.swipeTransition val progress = swipeTransition.progress val threshold = 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) { swipeTransition.cancelOffsetAnimation() layoutState.finishTransition(swipeTransition, swipeTransition.currentScene) val hasSnappedToIdle = layoutState.snapToIdleIfClose(threshold) if (hasSnappedToIdle) { // If the current swipe transition is closed to 0f or 1f, then we want to // interrupt the transition (snapping it to Idle) and scroll the list. return@PriorityNestedScrollConnection false } // Start only if we cannot consume this event val canStart = !shouldSnapToIdle if (canStart) { // If the current swipe transition is *not* closed to 0f or 1f, then we want the // scroll events to intercept the current transition to continue the scene // transition. isIntercepting = true } canStart true }, canStartPostScroll = { offsetAvailable, offsetBeforeStart -> val behavior: NestedScrollBehavior = Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +25 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel Loading Loading @@ -236,6 +237,30 @@ internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) : transitionState = TransitionState.Idle(idleScene) } } /** * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap * to the closest scene. * * @return true if snapped to the closest scene. */ internal fun snapToIdleIfClose(threshold: Float): Boolean { val transition = currentTransition ?: return false val progress = transition.progress fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold return when { isProgressCloseTo(0f) -> { finishTransition(transition, transition.fromScene) true } isProgressCloseTo(1f) -> { finishTransition(transition, transition.toScene) true } else -> false } } } /** Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +38 −0 Original line number Diff line number Diff line Loading @@ -158,4 +158,42 @@ class SceneTransitionLayoutStateTest { .isNotNull() assertThat(state.transformationSpec.transformations).hasSize(2) } @Test fun snapToIdleIfClose_snapToStart() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty) state.startTransition( transition(from = TestScenes.SceneA, to = TestScenes.SceneB, progress = { 0.2f }), transitionKey = null ) assertThat(state.isTransitioning()).isTrue() // Ignore the request if the progress is not close to 0 or 1, using the threshold. assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse() assertThat(state.isTransitioning()).isTrue() // Go to the initial scene if it is close to 0. assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue() assertThat(state.isTransitioning()).isFalse() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA)) } @Test fun snapToIdleIfClose_snapToEnd() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty) state.startTransition( transition(from = TestScenes.SceneA, to = TestScenes.SceneB, progress = { 0.8f }), transitionKey = null ) assertThat(state.isTransitioning()).isTrue() // Ignore the request if the progress is not close to 0 or 1, using the threshold. assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse() assertThat(state.isTransitioning()).isTrue() // Go to the final scene if it is close to 1. assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue() assertThat(state.isTransitioning()).isFalse() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB)) } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +10 −18 Original line number Diff line number Diff line Loading @@ -701,27 +701,19 @@ internal class SceneNestedScrollHandler( gestureHandler.shouldImmediatelyIntercept(startedPosition = null) if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false val swipeTransition = gestureHandler.swipeTransition val progress = swipeTransition.progress val threshold = 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) { swipeTransition.cancelOffsetAnimation() layoutState.finishTransition(swipeTransition, swipeTransition.currentScene) val hasSnappedToIdle = layoutState.snapToIdleIfClose(threshold) if (hasSnappedToIdle) { // If the current swipe transition is closed to 0f or 1f, then we want to // interrupt the transition (snapping it to Idle) and scroll the list. return@PriorityNestedScrollConnection false } // Start only if we cannot consume this event val canStart = !shouldSnapToIdle if (canStart) { // If the current swipe transition is *not* closed to 0f or 1f, then we want the // scroll events to intercept the current transition to continue the scene // transition. isIntercepting = true } canStart true }, canStartPostScroll = { offsetAvailable, offsetBeforeStart -> val behavior: NestedScrollBehavior = Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +25 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel Loading Loading @@ -236,6 +237,30 @@ internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) : transitionState = TransitionState.Idle(idleScene) } } /** * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap * to the closest scene. * * @return true if snapped to the closest scene. */ internal fun snapToIdleIfClose(threshold: Float): Boolean { val transition = currentTransition ?: return false val progress = transition.progress fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold return when { isProgressCloseTo(0f) -> { finishTransition(transition, transition.fromScene) true } isProgressCloseTo(1f) -> { finishTransition(transition, transition.toScene) true } else -> false } } } /** Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +38 −0 Original line number Diff line number Diff line Loading @@ -158,4 +158,42 @@ class SceneTransitionLayoutStateTest { .isNotNull() assertThat(state.transformationSpec.transformations).hasSize(2) } @Test fun snapToIdleIfClose_snapToStart() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty) state.startTransition( transition(from = TestScenes.SceneA, to = TestScenes.SceneB, progress = { 0.2f }), transitionKey = null ) assertThat(state.isTransitioning()).isTrue() // Ignore the request if the progress is not close to 0 or 1, using the threshold. assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse() assertThat(state.isTransitioning()).isTrue() // Go to the initial scene if it is close to 0. assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue() assertThat(state.isTransitioning()).isFalse() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA)) } @Test fun snapToIdleIfClose_snapToEnd() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(TestScenes.SceneA, SceneTransitions.Empty) state.startTransition( transition(from = TestScenes.SceneA, to = TestScenes.SceneB, progress = { 0.8f }), transitionKey = null ) assertThat(state.isTransitioning()).isTrue() // Ignore the request if the progress is not close to 0 or 1, using the threshold. assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse() assertThat(state.isTransitioning()).isTrue() // Go to the final scene if it is close to 1. assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue() assertThat(state.isTransitioning()).isFalse() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB)) } }