diff options
11 files changed, 117 insertions, 37 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index aae61bd0f554..b77a60b5b5d6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -506,7 +506,10 @@ private inline fun <T> computeValue(      // There is no ongoing transition.      if (state !is TransitionState.Transition || state.fromScene == state.toScene) { -        return idleValue +        // Even if this element SceneTransitionLayout is not animated, the layout itself might be +        // animated (e.g. by another parent SceneTransitionLayout), in which case this element still +        // need to participate in the layout phase. +        return currentValue()      }      // A transition was started but it's not ready yet (not all elements have been composed/laid diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt index 216608aff0cb..5d8eaf7f3d15 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt @@ -2,9 +2,10 @@ package com.android.compose.animation.scene  import androidx.compose.ui.geometry.Offset  import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.unit.IntSize  interface DraggableHandler { -    fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1) +    fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int = 1)      fun onDelta(pixels: Float)      fun onDragStopped(velocity: Float)  } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index d0a5f5bfebc0..d48781a4529b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.input.pointer.positionChange  import androidx.compose.ui.input.pointer.util.VelocityTracker  import androidx.compose.ui.input.pointer.util.addPointerInputChange  import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.unit.IntSize  import androidx.compose.ui.unit.Velocity  import androidx.compose.ui.util.fastForEach @@ -60,7 +61,7 @@ internal fun Modifier.multiPointerDraggable(      orientation: Orientation,      enabled: Boolean,      startDragImmediately: Boolean, -    onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit, +    onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,      onDragDelta: (Float) -> Unit,      onDragStopped: (velocity: Float) -> Unit,  ): Modifier = composed { @@ -83,7 +84,7 @@ internal fun Modifier.multiPointerDraggable(          val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->              velocityTracker.resetTracking() -            onDragStarted(startedPosition, pointersDown) +            onDragStarted(size, startedPosition, pointersDown)          }          val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index eb5168bdd3cb..1a79522da05d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -25,8 +25,9 @@ import androidx.compose.runtime.mutableFloatStateOf  import androidx.compose.runtime.mutableStateOf  import androidx.compose.runtime.setValue  import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.ui.ExperimentalComposeUiApi  import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.intermediateLayout  import androidx.compose.ui.platform.testTag  import androidx.compose.ui.unit.IntSize  import androidx.compose.ui.zIndex @@ -44,14 +45,24 @@ internal class Scene(      var content by mutableStateOf(content)      var userActions by mutableStateOf(actions)      var zIndex by mutableFloatStateOf(zIndex) -    var size by mutableStateOf(IntSize.Zero) +    var targetSize by mutableStateOf(IntSize.Zero)      /** The shared values in this scene that are not tied to a specific element. */      val sharedValues = SnapshotStateMap<ValueKey, Element.SharedValue<*>>()      @Composable +    @OptIn(ExperimentalComposeUiApi::class)      fun Content(modifier: Modifier = Modifier) { -        Box(modifier.zIndex(zIndex).onPlaced { size = it.size }.testTag(key.testTag)) { +        Box( +            modifier +                .zIndex(zIndex) +                .intermediateLayout { measurable, constraints -> +                    targetSize = lookaheadSize +                    val placeable = measurable.measure(constraints) +                    layout(placeable.width, placeable.height) { placeable.place(0, 0) } +                } +                .testTag(key.testTag) +        ) {              scope.content()          }      } 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 9a3a0aef30cb..9d71801be25b 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 @@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableFloatStateOf  import androidx.compose.runtime.mutableStateOf  import androidx.compose.runtime.setValue  import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntSize  import androidx.compose.ui.unit.Velocity  import androidx.compose.ui.unit.dp  import androidx.compose.ui.unit.round @@ -90,7 +91,7 @@ class SceneGestureHandler(      internal var gestureWithPriority: Any? = null -    internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) { +    internal fun onDragStarted(pointersDown: Int, layoutSize: IntSize, startedPosition: Offset?) {          if (isDrivingTransition) {              // This [transition] was already driving the animation: simply take over it.              // Stop animating and start from where the current offset. @@ -126,14 +127,14 @@ class SceneGestureHandler(          // we will also have to make sure that we correctly handle overscroll.          swipeTransition.absoluteDistance =              when (orientation) { -                Orientation.Horizontal -> layoutImpl.size.width -                Orientation.Vertical -> layoutImpl.size.height +                Orientation.Horizontal -> layoutSize.width +                Orientation.Vertical -> layoutSize.height              }.toFloat()          val fromEdge =              startedPosition?.let { position ->                  layoutImpl.edgeDetector.edge( -                    layoutImpl.size, +                    layoutSize,                      position.round(),                      layoutImpl.density,                      orientation, @@ -513,9 +514,9 @@ class SceneGestureHandler(  private class SceneDraggableHandler(      private val gestureHandler: SceneGestureHandler,  ) : DraggableHandler { -    override fun onDragStarted(startedPosition: Offset, pointersDown: Int) { +    override fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) {          gestureHandler.gestureWithPriority = this -        gestureHandler.onDragStarted(pointersDown, startedPosition) +        gestureHandler.onDragStarted(pointersDown, layoutSize, startedPosition)      }      override fun onDelta(pixels: Float) { @@ -647,7 +648,11 @@ class SceneNestedScrollHandler(              canContinueScroll = { true },              onStart = {                  gestureHandler.gestureWithPriority = this -                gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null) +                gestureHandler.onDragStarted( +                    pointersDown = 1, +                    layoutSize = gestureHandler.currentScene.targetSize, +                    startedPosition = null, +                )              },              onScroll = { offsetAvailable ->                  if (gestureHandler.gestureWithPriority != this) { 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 6edd1b6b923d..0b06953bc8e2 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 @@ -30,13 +30,15 @@ import androidx.compose.runtime.mutableStateOf  import androidx.compose.runtime.remember  import androidx.compose.runtime.setValue  import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.ui.ExperimentalComposeUiApi  import androidx.compose.ui.Modifier  import androidx.compose.ui.draw.drawWithContent  import androidx.compose.ui.layout.LookaheadScope -import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.intermediateLayout  import androidx.compose.ui.unit.Density  import androidx.compose.ui.unit.IntSize  import androidx.compose.ui.util.fastForEach +import com.android.compose.ui.util.lerp  import kotlinx.coroutines.CoroutineScope  import kotlinx.coroutines.channels.Channel @@ -64,12 +66,6 @@ class SceneTransitionLayoutImpl(      private val horizontalGestureHandler: SceneGestureHandler      private val verticalGestureHandler: SceneGestureHandler -    /** -     * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have -     * any scene configured or right before the first measure pass of the layout. -     */ -    @VisibleForTesting var size by mutableStateOf(IntSize.Zero) -      init {          setScenes(builder) @@ -157,15 +153,46 @@ class SceneTransitionLayoutImpl(      }      @Composable +    @OptIn(ExperimentalComposeUiApi::class)      internal fun Content(modifier: Modifier) {          Box(              modifier                  // Handle horizontal and vertical swipes on this layout.                  // Note: order here is important and will give a slight priority to the vertical                  // swipes. -                .swipeToScene(gestureHandler(Orientation.Horizontal)) -                .swipeToScene(gestureHandler(Orientation.Vertical)) -                .onSizeChanged { size = it } +                .swipeToScene(horizontalGestureHandler) +                .swipeToScene(verticalGestureHandler) +                // Animate the size of this layout. +                .intermediateLayout { measurable, constraints -> +                    // Measure content normally. +                    val placeable = measurable.measure(constraints) + +                    val width: Int +                    val height: Int +                    val state = state.transitionState +                    if (state !is TransitionState.Transition || state.fromScene == state.toScene) { +                        width = placeable.width +                        height = placeable.height +                    } else { +                        // Interpolate the size. +                        val fromSize = scene(state.fromScene).targetSize +                        val toSize = scene(state.toScene).targetSize + +                        // Optimization: make sure we don't read state.progress if fromSize == +                        // toSize to avoid running this code every frame when the layout size does +                        // not change. +                        if (fromSize == toSize) { +                            width = fromSize.width +                            height = fromSize.height +                        } else { +                            val size = lerp(fromSize, toSize, state.progress) +                            width = size.width.coerceAtLeast(0) +                            height = size.height.coerceAtLeast(0) +                        } +                    } + +                    layout(width, height) { placeable.place(0, 0) } +                }          ) {              LookaheadScope {                  val scenesToCompose = @@ -230,4 +257,9 @@ class SceneTransitionLayoutImpl(      }      internal fun isSceneReady(scene: SceneKey): Boolean = readyScenes.containsKey(scene) + +    @VisibleForTesting +    fun setScenesTargetSizeForTest(size: IntSize) { +        scenes.values.forEach { it.targetSize = size } +    }  } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt index 840800d838db..70534dde4f6f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt @@ -38,7 +38,7 @@ internal class EdgeTranslate(          transition: TransitionState.Transition,          value: Offset      ): Offset { -        val sceneSize = scene.size +        val sceneSize = scene.targetSize          val elementSize = sceneValues.targetSize          if (elementSize == Element.SizeUnspecified) {              return value 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 1e3d01108103..7ab2096b3d88 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 @@ -46,6 +46,7 @@ import org.junit.Test  import org.junit.runner.RunWith  private const val SCREEN_SIZE = 100f +private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt())  @RunWith(AndroidJUnit4::class)  class SceneGestureHandlerTest { @@ -80,7 +81,7 @@ class SceneGestureHandlerTest {                              edgeDetector = DefaultEdgeDetector,                              coroutineScope = coroutineScope,                          ) -                        .also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) }, +                        .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) },                  orientation = Orientation.Vertical,                  coroutineScope = coroutineScope,              ) @@ -128,18 +129,21 @@ class SceneGestureHandlerTest {          runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }      } +    private fun DraggableHandler.onDragStarted() = +        onDragStarted(layoutSize = LAYOUT_SIZE, startedPosition = Offset.Zero) +      @Test      fun testPreconditions() = runGestureTest { assertScene(currentScene = SceneA, isIdle = true) }      @Test      fun onDragStarted_shouldStartATransition() = runGestureTest { -        draggable.onDragStarted(startedPosition = Offset.Zero) +        draggable.onDragStarted()          assertScene(currentScene = SceneA, isIdle = false)      }      @Test      fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { -        draggable.onDragStarted(startedPosition = Offset.Zero) +        draggable.onDragStarted()          assertScene(currentScene = SceneA, isIdle = false)          val transition = transitionState as Transition @@ -152,7 +156,7 @@ class SceneGestureHandlerTest {      @Test      fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { -        draggable.onDragStarted(startedPosition = Offset.Zero) +        draggable.onDragStarted()          assertScene(currentScene = SceneA, isIdle = false)          draggable.onDelta(pixels = deltaInPixels10) @@ -170,7 +174,7 @@ class SceneGestureHandlerTest {      @Test      fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { -        draggable.onDragStarted(startedPosition = Offset.Zero) +        draggable.onDragStarted()          assertScene(currentScene = SceneA, isIdle = false)          draggable.onDelta(pixels = deltaInPixels10) @@ -188,7 +192,7 @@ class SceneGestureHandlerTest {      @Test      fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest { -        draggable.onDragStarted(startedPosition = Offset.Zero) +        draggable.onDragStarted()          assertScene(currentScene = SceneA, isIdle = false)          draggable.onDragStopped(velocity = 0f) @@ -197,7 +201,7 @@ class SceneGestureHandlerTest {      @Test      fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { -        draggable.onDragStarted(startedPosition = Offset.Zero) +        draggable.onDragStarted()          assertScene(currentScene = SceneA, isIdle = false)          draggable.onDelta(pixels = deltaInPixels10) @@ -217,7 +221,7 @@ class SceneGestureHandlerTest {          assertScene(currentScene = SceneC, isIdle = false)          // Start a new gesture while the offset is animating -        draggable.onDragStarted(startedPosition = Offset.Zero) +        draggable.onDragStarted()          assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()      } @@ -421,6 +425,7 @@ class SceneGestureHandlerTest {          draggable.onDelta(deltaInPixels10)          assertScene(currentScene = SceneA, isIdle = true)      } +      @Test      fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {          draggable.onDragStopped(velocityThreshold) @@ -437,7 +442,7 @@ class SceneGestureHandlerTest {      @Test      fun startNestedScrollWhileDragging() = runGestureTest {          val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always) -        draggable.onDragStarted(Offset.Zero) +        draggable.onDragStarted()          assertScene(currentScene = SceneA, isIdle = false)          val transition = transitionState as Transition diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 5afd420a5e16..321cf637824a 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -18,6 +18,8 @@ package com.android.compose.animation.scene  import androidx.activity.ComponentActivity  import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween  import androidx.compose.foundation.background  import androidx.compose.foundation.layout.Box  import androidx.compose.foundation.layout.fillMaxSize @@ -48,6 +50,7 @@ import androidx.compose.ui.unit.DpOffset  import androidx.compose.ui.unit.IntOffset  import androidx.compose.ui.unit.dp  import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.test.assertSizeIsEqualTo  import com.android.compose.test.subjects.DpOffsetSubject  import com.android.compose.test.subjects.assertThat  import com.google.common.truth.Truth.assertThat @@ -307,6 +310,26 @@ class SceneTransitionLayoutTest {          assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)      } +    @Test +    fun layoutSizeIsAnimated() { +        val layoutTag = "layout" +        rule.testTransition( +            fromSceneContent = { Box(Modifier.size(200.dp, 100.dp)) }, +            toSceneContent = { Box(Modifier.size(120.dp, 140.dp)) }, +            transition = { +                // 4 frames of animation. +                spec = tween(4 * 16, easing = LinearEasing) +            }, +            layoutModifier = Modifier.testTag(layoutTag), +        ) { +            before { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(200.dp, 100.dp) } +            at(16) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(180.dp, 110.dp) } +            at(32) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(160.dp, 120.dp) } +            at(48) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(140.dp, 130.dp) } +            after { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(120.dp, 140.dp) } +        } +    } +      private fun SemanticsNodeInteraction.offsetRelativeTo(          other: SemanticsNodeInteraction,      ): DpOffset { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt index 2a27763f1d5c..8cffcf6980cc 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt @@ -48,7 +48,7 @@ class EdgeTranslateTest {          rule.testTransition(              // The layout under test is 300dp x 300dp.              layoutModifier = Modifier.size(300.dp), -            fromSceneContent = {}, +            fromSceneContent = { Box(Modifier.fillMaxSize()) },              toSceneContent = {                  // Foo is 100dp x 100dp in the center of the layout, so at offset = (100dp, 100dp)                  Box(Modifier.fillMaxSize()) { diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt index e0ae1be69aaf..06de2965f716 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt @@ -16,7 +16,6 @@  package com.android.compose.animation.scene -import androidx.compose.foundation.layout.fillMaxSize  import androidx.compose.runtime.Composable  import androidx.compose.runtime.getValue  import androidx.compose.runtime.mutableStateOf @@ -104,7 +103,7 @@ fun ComposeContentTestRule.testTransition(                  currentScene,                  onChangeScene,                  transitions { from(fromScene, to = toScene, transition) }, -                layoutModifier.fillMaxSize(), +                layoutModifier,              ) {                  scene(fromScene, content = fromSceneContent)                  scene(toScene, content = toSceneContent)  |