Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +24 −5 Original line number Diff line number Diff line Loading @@ -605,6 +605,8 @@ private class SwipeTransition( override val isInitiatedByUserInput = true override var bouncingScene: SceneKey? = null /** The current offset caused by the drag gesture. */ var dragOffset by mutableFloatStateOf(0f) Loading Loading @@ -694,14 +696,31 @@ private class SwipeTransition( ): OffsetAnimation { return startOffsetAnimation { val animatable = Animatable(dragOffset, OffsetVisibilityThreshold) val isTargetGreater = targetOffset > animatable.value val job = coroutineScope .launch { try { animatable.animateTo( targetValue = targetOffset, animationSpec = swipeSpec, initialVelocity = initialVelocity, ) ) { if (bouncingScene == null) { val isBouncing = if (isTargetGreater) { value > targetOffset } else { value < targetOffset } if (isBouncing) { bouncingScene = targetScene } } } } finally { bouncingScene = null } } // Make sure that we settle to target scene at the end of the animation or if // the animation is cancelled. Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +2 −1 Original line number Diff line number Diff line Loading @@ -588,7 +588,8 @@ private inline fun <T> computeValue( // TODO(b/290184746): Make sure that we don't overflow transformations associated to a // range. val directionSign = if (transition.isUpOrLeft) -1 else 1 val overscrollProgress = transition.progress.let { if (it > 1f) it - 1f else it } val isToScene = overscroll.scene == transition.toScene val overscrollProgress = transition.progress.let { if (isToScene) it - 1f else it } val progress = directionSign * overscrollProgress val rangeProgress = propertySpec.range?.progress(progress) ?: progress Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +9 −2 Original line number Diff line number Diff line Loading @@ -255,6 +255,12 @@ sealed interface TransitionState { */ val overscrollScope: OverscrollScope /** * The scene around which the transition is currently bouncing. When not `null`, this * transition is currently oscillating around this scene and will soon settle to that scene. */ val bouncingScene: SceneKey? companion object { const val DistanceUnspecified = 0f } Loading Loading @@ -287,9 +293,10 @@ internal abstract class BaseSceneTransitionLayoutState( val transition = currentTransition ?: return null if (transition !is TransitionState.HasOverscrollProperties) return null val progress = transition.progress val bouncingScene = transition.bouncingScene return when { progress < 0f -> fromOverscrollSpec progress > 1f -> toOverscrollSpec progress < 0f || bouncingScene == transition.fromScene -> fromOverscrollSpec progress > 1f || bouncingScene == transition.toScene -> toOverscrollSpec else -> null } } Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +53 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.Orientation Loading Loading @@ -752,4 +754,55 @@ class ElementTest { assertThat(state.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f) } @Test fun elementTransitionWithDistanceDuringOverscrollBouncing() { val layoutWidth = 200.dp val layoutHeight = 400.dp val state = setupOverscrollScenario( layoutWidth = layoutWidth, layoutHeight = layoutHeight, sceneTransitions = { defaultSwipeSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow, ) overscroll(TestScenes.SceneB, Orientation.Vertical) { // On overscroll 100% -> Foo should translate by layoutHeight translate(TestElements.Foo, y = { absoluteDistance }) } }, firstScroll = 1f, // 100% scroll ) val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) fooElement.assertTopPositionInRootIsEqualTo(0.dp) rule.onRoot().performTouchInput { // Scroll another 50% moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000) } val transition = state.currentTransition assertThat(transition).isNotNull() transition as TransitionState.HasOverscrollProperties // Scroll 150% (100% scroll + 50% overscroll) assertThat(transition.progress).isEqualTo(1.5f) assertThat(state.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f)) // finger raised rule.onRoot().performTouchInput { up() } // The target value is 1f, but the spring (defaultSwipeSpec) allows you to go to a lower // value. rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f } assertThat(state.currentOverscrollSpec).isNotNull() assertThat(transition.bouncingScene).isEqualTo(transition.toScene) } } packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt +2 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ fun transition( isInitiatedByUserInput: Boolean = false, isUserInputOngoing: Boolean = false, isUpOrLeft: Boolean = false, bouncingScene: SceneKey? = null, orientation: Orientation = Orientation.Horizontal, ): TransitionState.Transition { return object : TransitionState.Transition(from, to), TransitionState.HasOverscrollProperties { Loading @@ -37,6 +38,7 @@ fun transition( override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput override val isUserInputOngoing: Boolean = isUserInputOngoing override val isUpOrLeft: Boolean = isUpOrLeft override val bouncingScene: SceneKey? = bouncingScene override val orientation: Orientation = orientation override val overscrollScope: OverscrollScope = object : OverscrollScope { Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +24 −5 Original line number Diff line number Diff line Loading @@ -605,6 +605,8 @@ private class SwipeTransition( override val isInitiatedByUserInput = true override var bouncingScene: SceneKey? = null /** The current offset caused by the drag gesture. */ var dragOffset by mutableFloatStateOf(0f) Loading Loading @@ -694,14 +696,31 @@ private class SwipeTransition( ): OffsetAnimation { return startOffsetAnimation { val animatable = Animatable(dragOffset, OffsetVisibilityThreshold) val isTargetGreater = targetOffset > animatable.value val job = coroutineScope .launch { try { animatable.animateTo( targetValue = targetOffset, animationSpec = swipeSpec, initialVelocity = initialVelocity, ) ) { if (bouncingScene == null) { val isBouncing = if (isTargetGreater) { value > targetOffset } else { value < targetOffset } if (isBouncing) { bouncingScene = targetScene } } } } finally { bouncingScene = null } } // Make sure that we settle to target scene at the end of the animation or if // the animation is cancelled. Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +2 −1 Original line number Diff line number Diff line Loading @@ -588,7 +588,8 @@ private inline fun <T> computeValue( // TODO(b/290184746): Make sure that we don't overflow transformations associated to a // range. val directionSign = if (transition.isUpOrLeft) -1 else 1 val overscrollProgress = transition.progress.let { if (it > 1f) it - 1f else it } val isToScene = overscroll.scene == transition.toScene val overscrollProgress = transition.progress.let { if (isToScene) it - 1f else it } val progress = directionSign * overscrollProgress val rangeProgress = propertySpec.range?.progress(progress) ?: progress Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +9 −2 Original line number Diff line number Diff line Loading @@ -255,6 +255,12 @@ sealed interface TransitionState { */ val overscrollScope: OverscrollScope /** * The scene around which the transition is currently bouncing. When not `null`, this * transition is currently oscillating around this scene and will soon settle to that scene. */ val bouncingScene: SceneKey? companion object { const val DistanceUnspecified = 0f } Loading Loading @@ -287,9 +293,10 @@ internal abstract class BaseSceneTransitionLayoutState( val transition = currentTransition ?: return null if (transition !is TransitionState.HasOverscrollProperties) return null val progress = transition.progress val bouncingScene = transition.bouncingScene return when { progress < 0f -> fromOverscrollSpec progress > 1f -> toOverscrollSpec progress < 0f || bouncingScene == transition.fromScene -> fromOverscrollSpec progress > 1f || bouncingScene == transition.toScene -> toOverscrollSpec else -> null } } Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +53 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.Orientation Loading Loading @@ -752,4 +754,55 @@ class ElementTest { assertThat(state.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f) } @Test fun elementTransitionWithDistanceDuringOverscrollBouncing() { val layoutWidth = 200.dp val layoutHeight = 400.dp val state = setupOverscrollScenario( layoutWidth = layoutWidth, layoutHeight = layoutHeight, sceneTransitions = { defaultSwipeSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow, ) overscroll(TestScenes.SceneB, Orientation.Vertical) { // On overscroll 100% -> Foo should translate by layoutHeight translate(TestElements.Foo, y = { absoluteDistance }) } }, firstScroll = 1f, // 100% scroll ) val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) fooElement.assertTopPositionInRootIsEqualTo(0.dp) rule.onRoot().performTouchInput { // Scroll another 50% moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000) } val transition = state.currentTransition assertThat(transition).isNotNull() transition as TransitionState.HasOverscrollProperties // Scroll 150% (100% scroll + 50% overscroll) assertThat(transition.progress).isEqualTo(1.5f) assertThat(state.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f)) // finger raised rule.onRoot().performTouchInput { up() } // The target value is 1f, but the spring (defaultSwipeSpec) allows you to go to a lower // value. rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f } assertThat(state.currentOverscrollSpec).isNotNull() assertThat(transition.bouncingScene).isEqualTo(transition.toScene) } }
packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt +2 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ fun transition( isInitiatedByUserInput: Boolean = false, isUserInputOngoing: Boolean = false, isUpOrLeft: Boolean = false, bouncingScene: SceneKey? = null, orientation: Orientation = Orientation.Horizontal, ): TransitionState.Transition { return object : TransitionState.Transition(from, to), TransitionState.HasOverscrollProperties { Loading @@ -37,6 +38,7 @@ fun transition( override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput override val isUserInputOngoing: Boolean = isUserInputOngoing override val isUpOrLeft: Boolean = isUpOrLeft override val bouncingScene: SceneKey? = bouncingScene override val orientation: Orientation = orientation override val overscrollScope: OverscrollScope = object : OverscrollScope { Loading