Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +2 −2 Original line number Diff line number Diff line Loading @@ -1088,8 +1088,8 @@ private inline fun <T> computeValue( // range. val directionSign = if (transition.isUpOrLeft) -1 else 1 val isToScene = overscroll.scene == transition.toScene val overscrollProgress = transition.progress.let { if (isToScene) it - 1f else it } val progress = directionSign * overscrollProgress val linearProgress = transition.progress.let { if (isToScene) it - 1f else it } val progress = directionSign * overscroll.progressConverter(linearProgress) val rangeProgress = propertySpec.range?.progress(progress) ?: progress // Interpolate between the value at rest and the over scrolled value. Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +15 −3 Original line number Diff line number Diff line Loading @@ -48,7 +48,8 @@ internal constructor( ) { private val transitionCache = mutableMapOf< SceneKey, MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>> SceneKey, MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>> >() private val overscrollCache = Loading Loading @@ -87,8 +88,7 @@ internal constructor( return transition(from, to, key) { (it.from == to && it.to == null) || (it.to == from && it.from == null) } ?.reversed() ?: defaultTransition(from, to) ?.reversed() ?: defaultTransition(from, to) } private fun transition( Loading Loading @@ -257,12 +257,24 @@ interface OverscrollSpec { /** The [TransformationSpec] associated to this [OverscrollSpec]. */ val transformationSpec: TransformationSpec /** * Function that takes a linear overscroll progress value ranging from 0 to +/- infinity and * outputs the desired **overscroll progress value**. * * When the progress value is: * - 0, the user is not overscrolling. * - 1, the user overscrolled by exactly the [OverscrollBuilder.distance]. * - Greater than 1, the user overscrolled more than the [OverscrollBuilder.distance]. */ val progressConverter: (Float) -> Float } internal class OverscrollSpecImpl( override val scene: SceneKey, override val orientation: Orientation, override val transformationSpec: TransformationSpecImpl, override val progressConverter: (Float) -> Float, ) : OverscrollSpec /** Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +11 −0 Original line number Diff line number Diff line Loading @@ -185,6 +185,17 @@ interface TransitionBuilder : BaseTransitionBuilder { @TransitionDsl interface OverscrollBuilder : BaseTransitionBuilder { /** * Function that takes a linear overscroll progress value ranging from 0 to +/- infinity and * outputs the desired **overscroll progress value**. * * When the progress value is: * - 0, the user is not overscrolling. * - 1, the user overscrolled by exactly the [distance]. * - Greater than 1, the user overscrolled more than the [distance]. */ var progressConverter: (Float) -> Float /** Translate the element(s) matching [matcher] by ([x], [y]) pixels. */ fun translate( matcher: ElementMatcher, Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +15 −9 Original line number Diff line number Diff line Loading @@ -81,16 +81,20 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { orientation: Orientation, builder: OverscrollBuilder.() -> Unit ): OverscrollSpec { fun transformationSpec(): TransformationSpecImpl { val impl = OverscrollBuilderImpl().apply(builder) return TransformationSpecImpl( val spec = OverscrollSpecImpl( scene = scene, orientation = orientation, transformationSpec = TransformationSpecImpl( progressSpec = snap(), swipeSpec = null, distance = impl.distance, transformations = impl.transformations, ), progressConverter = impl.progressConverter ) } val spec = OverscrollSpecImpl(scene, orientation, transformationSpec()) transitionOverscrollSpecs.add(spec) return spec } Loading Loading @@ -231,6 +235,8 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu } internal open class OverscrollBuilderImpl : BaseTransitionBuilderImpl(), OverscrollBuilder { override var progressConverter: (Float) -> Float = { it } override fun translate( matcher: ElementMatcher, x: OverscrollScope.() -> Float, Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +57 −0 Original line number Diff line number Diff line Loading @@ -888,6 +888,63 @@ class ElementTest { assertThat(animatedFloat).isEqualTo(100f) } @Test fun elementTransitionWithDistanceDuringOverscrollWithProgressConverter() { val layoutWidth = 200.dp val layoutHeight = 400.dp var animatedFloat = 0f val state = setupOverscrollScenario( layoutWidth = layoutWidth, layoutHeight = layoutHeight, sceneTransitions = { overscroll(SceneB, Orientation.Vertical) { // Overscroll progress will be halved progressConverter = { it / 2f } // On overscroll 100% -> Foo should translate by layoutHeight translate(TestElements.Foo, y = { absoluteDistance }) } }, firstScroll = 1f, // 100% scroll animatedFloatRange = 0f..100f, onAnimatedFloat = { animatedFloat = it }, ) val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag) fooElement.assertTopPositionInRootIsEqualTo(0.dp) assertThat(animatedFloat).isEqualTo(100f) rule.onRoot().performTouchInput { // Scroll another 100% moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) } val transition = assertThat(state.transitionState).isTransition() assertThat(animatedFloat).isEqualTo(100f) // Scroll 200% (100% scroll + 100% overscroll) assertThat(transition).hasProgress(2f) assertThat(transition).hasOverscrollSpec() // Overscroll progress is halved, we are at 50% of the overscroll progress. fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f) assertThat(animatedFloat).isEqualTo(100f) rule.onRoot().performTouchInput { // Scroll another 100% moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) } // Scroll 300% (100% scroll + 200% overscroll) assertThat(transition).hasProgress(3f) assertThat(transition).hasOverscrollSpec() // Overscroll progress is halved, we are at 100% of the overscroll progress. fooElement.assertTopPositionInRootIsEqualTo(layoutHeight) assertThat(animatedFloat).isEqualTo(100f) } @Test fun elementTransitionWithDistanceDuringOverscrollBouncing() { val layoutWidth = 200.dp Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +2 −2 Original line number Diff line number Diff line Loading @@ -1088,8 +1088,8 @@ private inline fun <T> computeValue( // range. val directionSign = if (transition.isUpOrLeft) -1 else 1 val isToScene = overscroll.scene == transition.toScene val overscrollProgress = transition.progress.let { if (isToScene) it - 1f else it } val progress = directionSign * overscrollProgress val linearProgress = transition.progress.let { if (isToScene) it - 1f else it } val progress = directionSign * overscroll.progressConverter(linearProgress) val rangeProgress = propertySpec.range?.progress(progress) ?: progress // Interpolate between the value at rest and the over scrolled value. Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +15 −3 Original line number Diff line number Diff line Loading @@ -48,7 +48,8 @@ internal constructor( ) { private val transitionCache = mutableMapOf< SceneKey, MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>> SceneKey, MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>> >() private val overscrollCache = Loading Loading @@ -87,8 +88,7 @@ internal constructor( return transition(from, to, key) { (it.from == to && it.to == null) || (it.to == from && it.from == null) } ?.reversed() ?: defaultTransition(from, to) ?.reversed() ?: defaultTransition(from, to) } private fun transition( Loading Loading @@ -257,12 +257,24 @@ interface OverscrollSpec { /** The [TransformationSpec] associated to this [OverscrollSpec]. */ val transformationSpec: TransformationSpec /** * Function that takes a linear overscroll progress value ranging from 0 to +/- infinity and * outputs the desired **overscroll progress value**. * * When the progress value is: * - 0, the user is not overscrolling. * - 1, the user overscrolled by exactly the [OverscrollBuilder.distance]. * - Greater than 1, the user overscrolled more than the [OverscrollBuilder.distance]. */ val progressConverter: (Float) -> Float } internal class OverscrollSpecImpl( override val scene: SceneKey, override val orientation: Orientation, override val transformationSpec: TransformationSpecImpl, override val progressConverter: (Float) -> Float, ) : OverscrollSpec /** Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +11 −0 Original line number Diff line number Diff line Loading @@ -185,6 +185,17 @@ interface TransitionBuilder : BaseTransitionBuilder { @TransitionDsl interface OverscrollBuilder : BaseTransitionBuilder { /** * Function that takes a linear overscroll progress value ranging from 0 to +/- infinity and * outputs the desired **overscroll progress value**. * * When the progress value is: * - 0, the user is not overscrolling. * - 1, the user overscrolled by exactly the [distance]. * - Greater than 1, the user overscrolled more than the [distance]. */ var progressConverter: (Float) -> Float /** Translate the element(s) matching [matcher] by ([x], [y]) pixels. */ fun translate( matcher: ElementMatcher, Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +15 −9 Original line number Diff line number Diff line Loading @@ -81,16 +81,20 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { orientation: Orientation, builder: OverscrollBuilder.() -> Unit ): OverscrollSpec { fun transformationSpec(): TransformationSpecImpl { val impl = OverscrollBuilderImpl().apply(builder) return TransformationSpecImpl( val spec = OverscrollSpecImpl( scene = scene, orientation = orientation, transformationSpec = TransformationSpecImpl( progressSpec = snap(), swipeSpec = null, distance = impl.distance, transformations = impl.transformations, ), progressConverter = impl.progressConverter ) } val spec = OverscrollSpecImpl(scene, orientation, transformationSpec()) transitionOverscrollSpecs.add(spec) return spec } Loading Loading @@ -231,6 +235,8 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu } internal open class OverscrollBuilderImpl : BaseTransitionBuilderImpl(), OverscrollBuilder { override var progressConverter: (Float) -> Float = { it } override fun translate( matcher: ElementMatcher, x: OverscrollScope.() -> Float, Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +57 −0 Original line number Diff line number Diff line Loading @@ -888,6 +888,63 @@ class ElementTest { assertThat(animatedFloat).isEqualTo(100f) } @Test fun elementTransitionWithDistanceDuringOverscrollWithProgressConverter() { val layoutWidth = 200.dp val layoutHeight = 400.dp var animatedFloat = 0f val state = setupOverscrollScenario( layoutWidth = layoutWidth, layoutHeight = layoutHeight, sceneTransitions = { overscroll(SceneB, Orientation.Vertical) { // Overscroll progress will be halved progressConverter = { it / 2f } // On overscroll 100% -> Foo should translate by layoutHeight translate(TestElements.Foo, y = { absoluteDistance }) } }, firstScroll = 1f, // 100% scroll animatedFloatRange = 0f..100f, onAnimatedFloat = { animatedFloat = it }, ) val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag) fooElement.assertTopPositionInRootIsEqualTo(0.dp) assertThat(animatedFloat).isEqualTo(100f) rule.onRoot().performTouchInput { // Scroll another 100% moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) } val transition = assertThat(state.transitionState).isTransition() assertThat(animatedFloat).isEqualTo(100f) // Scroll 200% (100% scroll + 100% overscroll) assertThat(transition).hasProgress(2f) assertThat(transition).hasOverscrollSpec() // Overscroll progress is halved, we are at 50% of the overscroll progress. fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f) assertThat(animatedFloat).isEqualTo(100f) rule.onRoot().performTouchInput { // Scroll another 100% moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) } // Scroll 300% (100% scroll + 200% overscroll) assertThat(transition).hasProgress(3f) assertThat(transition).hasOverscrollSpec() // Overscroll progress is halved, we are at 100% of the overscroll progress. fooElement.assertTopPositionInRootIsEqualTo(layoutHeight) assertThat(animatedFloat).isEqualTo(100f) } @Test fun elementTransitionWithDistanceDuringOverscrollBouncing() { val layoutWidth = 200.dp Loading