diff options
9 files changed, 170 insertions, 38 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 6114499a2f5e..63ec54fbef9c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round +import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified import com.android.compose.nestedscroll.PriorityNestedScrollConnection import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope @@ -353,10 +354,7 @@ private class DragControllerImpl( // If the swipe was not committed or if the swipe distance is not computed yet, don't do // anything. - if ( - swipeTransition._currentScene != toScene || - distance == SwipeTransition.DistanceUnspecified - ) { + if (swipeTransition._currentScene != toScene || distance == DistanceUnspecified) { return fromScene to 0f } @@ -418,7 +416,7 @@ private class DragControllerImpl( var targetScene: Scene var targetOffset: Float if ( - distance != SwipeTransition.DistanceUnspecified && + distance != DistanceUnspecified && shouldCommitSwipe( offset, distance, @@ -444,8 +442,8 @@ private class DragControllerImpl( if (targetScene == fromScene) { 0f } else { - check(distance != SwipeTransition.DistanceUnspecified) { - "distance is equal to ${SwipeTransition.DistanceUnspecified}" + check(distance != DistanceUnspecified) { + "distance is equal to $DistanceUnspecified" } distance } @@ -628,6 +626,12 @@ private class SwipeTransition( /** The spec to use when animating this transition to either [fromScene] or [toScene]. */ lateinit var swipeSpec: SpringSpec<Float> + override val overscrollScope: OverscrollScope = + object : OverscrollScope { + override val absoluteDistance: Float + get() = distance().absoluteValue + } + private var lastDistance = DistanceUnspecified /** Whether [TransitionState.Transition.finish] was called on this transition. */ @@ -753,10 +757,6 @@ private class SwipeTransition( /** The job in which [animatable] is animated. */ val job: Job, ) - - companion object { - const val DistanceUnspecified = 0f - } } private object DefaultSwipeDistance : UserActionDistance { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 86124df295b4..e6f5d585e915 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -236,19 +236,28 @@ sealed interface TransitionState { interface HasOverscrollProperties { /** - * The position of the [TransitionState.Transition.toScene]. + * The position of the [Transition.toScene]. * * Used to understand the direction of the overscroll. */ val isUpOrLeft: Boolean /** - * The relative orientation between [TransitionState.Transition.fromScene] and - * [TransitionState.Transition.toScene]. + * The relative orientation between [Transition.fromScene] and [Transition.toScene]. * * Used to understand the orientation of the overscroll. */ val orientation: Orientation + + /** + * Scope which can be used in the Overscroll DSL to define a transformation based on the + * distance between [Transition.fromScene] and [Transition.toScene]. + */ + val overscrollScope: OverscrollScope + + companion object { + const val DistanceUnspecified = 0f + } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index 2dd41cd329a2..b46614397ff4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -30,6 +30,7 @@ import com.android.compose.animation.scene.transformation.AnchoredTranslate import com.android.compose.animation.scene.transformation.DrawScale import com.android.compose.animation.scene.transformation.EdgeTranslate import com.android.compose.animation.scene.transformation.Fade +import com.android.compose.animation.scene.transformation.OverscrollTranslate import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.RangedPropertyTransformation import com.android.compose.animation.scene.transformation.ScaleSize @@ -124,7 +125,7 @@ internal constructor( overscrollSpecs.fastForEach { spec -> if (spec.orientation == orientation && filter(spec)) { if (match != null) { - error("Found multiple transition specs for transition $scene") + error("Found multiple overscroll specs for overscroll $scene") } match = spec } @@ -297,6 +298,7 @@ internal class TransformationSpecImpl( ) { when (current) { is Translate, + is OverscrollTranslate, is EdgeTranslate, is AnchoredTranslate -> { throwIfNotNull(offset, element, name = "offset") diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index bc52a28279dc..2c109a337f65 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */ fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions { @@ -88,8 +89,7 @@ interface SceneTransitionsBuilder { ): OverscrollSpec } -@TransitionDsl -interface OverscrollBuilder : PropertyTransformationBuilder { +interface BaseTransitionBuilder : PropertyTransformationBuilder { /** * The distance it takes for this transition to animate from 0% to 100% when it is driven by a * [UserAction]. @@ -120,7 +120,7 @@ interface OverscrollBuilder : PropertyTransformationBuilder { } @TransitionDsl -interface TransitionBuilder : OverscrollBuilder, PropertyTransformationBuilder { +interface TransitionBuilder : BaseTransitionBuilder { /** * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when * the transition is triggered (i.e. it is not gesture-based). @@ -176,6 +176,24 @@ interface TransitionBuilder : OverscrollBuilder, PropertyTransformationBuilder { fun reversed(builder: TransitionBuilder.() -> Unit) } +@TransitionDsl +interface OverscrollBuilder : BaseTransitionBuilder { + /** Translate the element(s) matching [matcher] by ([x], [y]) pixels. */ + fun translate( + matcher: ElementMatcher, + x: OverscrollScope.() -> Float = { 0f }, + y: OverscrollScope.() -> Float = { 0f }, + ) +} + +interface OverscrollScope { + /** + * Return the absolute distance between fromScene and toScene, if available, otherwise + * [DistanceUnspecified]. + */ + val absoluteDistance: Float +} + /** * An interface to decide where we should draw shared Elements or compose MovableElements. * diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 65e8ea5cc341..1c9080fa085d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -31,6 +31,7 @@ import com.android.compose.animation.scene.transformation.AnchoredTranslate import com.android.compose.animation.scene.transformation.DrawScale import com.android.compose.animation.scene.transformation.EdgeTranslate import com.android.compose.animation.scene.transformation.Fade +import com.android.compose.animation.scene.transformation.OverscrollTranslate import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.RangedPropertyTransformation import com.android.compose.animation.scene.transformation.ScaleSize @@ -114,7 +115,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { } } -internal open class OverscrollBuilderImpl : OverscrollBuilder { +internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { val transformations = mutableListOf<Transformation>() private var range: TransformationRange? = null protected var reversed = false @@ -130,7 +131,7 @@ internal open class OverscrollBuilderImpl : OverscrollBuilder { range = null } - private fun transformation(transformation: PropertyTransformation<*>) { + protected fun transformation(transformation: PropertyTransformation<*>) { val transformation = if (range != null) { RangedPropertyTransformation(transformation, range!!) @@ -185,7 +186,7 @@ internal open class OverscrollBuilderImpl : OverscrollBuilder { } } -internal class TransitionBuilderImpl : OverscrollBuilderImpl(), TransitionBuilder { +internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBuilder { override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow) override var swipeSpec: SpringSpec<Float>? = null override var distance: UserActionDistance? = null @@ -226,3 +227,13 @@ internal class TransitionBuilderImpl : OverscrollBuilderImpl(), TransitionBuilde fractionRange(start, end, builder) } } + +internal open class OverscrollBuilderImpl : BaseTransitionBuilderImpl(), OverscrollBuilder { + override fun translate( + matcher: ElementMatcher, + x: OverscrollScope.() -> Float, + y: OverscrollScope.() -> Float + ) { + transformation(OverscrollTranslate(matcher, x, y)) + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index 04d5914bff69..849c9d71ec2f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -21,11 +21,11 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher +import com.android.compose.animation.scene.OverscrollScope import com.android.compose.animation.scene.Scene import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.TransitionState -/** Translate an element by a fixed amount of density-independent pixels. */ internal class Translate( override val matcher: ElementMatcher, private val x: Dp = 0.dp, @@ -47,3 +47,28 @@ internal class Translate( } } } + +internal class OverscrollTranslate( + override val matcher: ElementMatcher, + val x: OverscrollScope.() -> Float = { 0f }, + val y: OverscrollScope.() -> Float = { 0f }, +) : PropertyTransformation<Offset> { + override fun transform( + layoutImpl: SceneTransitionLayoutImpl, + scene: Scene, + element: Element, + sceneState: Element.SceneState, + transition: TransitionState.Transition, + value: Offset, + ): Offset { + // As this object is created by OverscrollBuilderImpl and we retrieve the current + // OverscrollSpec only when the transition implements HasOverscrollProperties, we can assume + // that this method was invoked after performing this check. + val overscrollProperties = transition as TransitionState.HasOverscrollProperties + + return Offset( + x = value.x + overscrollProperties.overscrollScope.x(), + y = value.y + overscrollProperties.overscrollScope.y(), + ) + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 059a10e23207..26e01fefcb9b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -539,24 +539,20 @@ class ElementTest { } } - @Test - fun elementTransitionDuringOverscroll() { + private fun setupOverscrollScenario( + layoutWidth: Dp, + layoutHeight: Dp, + sceneTransitions: SceneTransitionsBuilder.() -> Unit, + firstScroll: Float + ): MutableSceneTransitionLayoutStateImpl { // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is // detected as a drag event. var touchSlop = 0f - val overscrollTranslateY = 10.dp - val layoutWidth = 200.dp - val layoutHeight = 400.dp val state = MutableSceneTransitionLayoutState( initialScene = TestScenes.SceneA, - transitions = - transitions { - overscroll(TestScenes.SceneB, Orientation.Vertical) { - translate(TestElements.Foo, y = overscrollTranslateY) - } - } + transitions = transitions(sceneTransitions), ) as MutableSceneTransitionLayoutStateImpl @@ -585,9 +581,30 @@ class ElementTest { rule.onRoot().performTouchInput { val middleTop = Offset((layoutWidth / 2).toPx(), 0f) down(middleTop) - // Scroll 50% - moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000) + val firstScrollHeight = layoutHeight.toPx() * firstScroll + moveBy(Offset(0f, touchSlop + firstScrollHeight), delayMillis = 1_000) } + return state + } + + @Test + fun elementTransitionDuringOverscroll() { + val layoutWidth = 200.dp + val layoutHeight = 400.dp + val overscrollTranslateY = 10.dp + + val state = + setupOverscrollScenario( + layoutWidth = layoutWidth, + layoutHeight = layoutHeight, + sceneTransitions = { + overscroll(TestScenes.SceneB, Orientation.Vertical) { + // On overscroll 100% -> Foo should translate by overscrollTranslateY + translate(TestElements.Foo, y = overscrollTranslateY) + } + }, + firstScroll = 0.5f, // Scroll 50% + ) val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) fooElement.assertTopPositionInRootIsEqualTo(0.dp) @@ -691,4 +708,48 @@ class ElementTest { assertThat(state.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) } + + @Test + fun elementTransitionWithDistanceDuringOverscroll() { + val layoutWidth = 200.dp + val layoutHeight = 400.dp + val state = + setupOverscrollScenario( + layoutWidth = layoutWidth, + layoutHeight = layoutHeight, + sceneTransitions = { + 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() + + // Scroll 150% (100% scroll + 50% overscroll) + assertThat(transition!!.progress).isEqualTo(1.5f) + assertThat(state.currentOverscrollSpec).isNotNull() + fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f) + + rule.onRoot().performTouchInput { + // Scroll another 100% + moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) + } + + // Scroll 250% (100% scroll + 150% overscroll) + assertThat(transition.progress).isEqualTo(2.5f) + assertThat(state.currentOverscrollSpec).isNotNull() + fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt index c9c3eccdedfc..825fe138c3c4 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -22,9 +22,9 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.Orientation import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.transformation.OverscrollTranslate import com.android.compose.animation.scene.transformation.Transformation import com.android.compose.animation.scene.transformation.TransformationRange -import com.android.compose.animation.scene.transformation.Translate import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -228,12 +228,14 @@ class TransitionDslTest { @Test fun overscrollSpec() { val transitions = transitions { - overscroll(TestScenes.SceneA, Orientation.Vertical) { translate(TestElements.Bar) } + overscroll(TestScenes.SceneA, Orientation.Vertical) { + translate(TestElements.Bar, x = { 1f }, y = { 2f }) + } } val overscrollSpec = transitions.overscrollSpecs.single() val transformation = overscrollSpec.transformationSpec.transformations.single() - assertThat(transformation).isInstanceOf(Translate::class.java) + assertThat(transformation).isInstanceOf(OverscrollTranslate::class.java) } companion object { diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt index 153d2b8769b3..73a66c629024 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt @@ -38,6 +38,10 @@ fun transition( override val isUserInputOngoing: Boolean = isUserInputOngoing override val isUpOrLeft: Boolean = isUpOrLeft override val orientation: Orientation = orientation + override val overscrollScope: OverscrollScope = + object : OverscrollScope { + override val absoluteDistance = 0f + } override fun finish(): Job { error("finish() is not supported in test transitions") |