Merge "Implement the default pane motions" into androidx-main
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
index f559d54..1c455bf 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
@@ -16,6 +16,16 @@
package androidx.compose.material3.adaptive.layout
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandHorizontally
+import androidx.compose.animation.shrinkHorizontally
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.AnimateBounds
import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.EnterFromLeft
@@ -29,6 +39,14 @@
import androidx.compose.material3.adaptive.layout.DefaultPaneMotion.Companion.NoMotion
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion.Expanded
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion.Hidden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Test
import org.junit.runner.RunWith
@@ -49,6 +67,38 @@
}
}
}
+
+ @Test
+ fun test_allDefaultPaneMotionTransitions() {
+ NoMotion.assertTransitions(EnterTransition.None, ExitTransition.None)
+ EnterFromLeft.assertTransitions(mockEnterFromLeftTransition, ExitTransition.None)
+ EnterFromRight.assertTransitions(mockEnterFromRightTransition, ExitTransition.None)
+ EnterFromLeftDelayed.assertTransitions(
+ mockEnterFromLeftDelayedTransition,
+ ExitTransition.None
+ )
+ EnterFromRightDelayed.assertTransitions(
+ mockEnterFromRightDelayedTransition,
+ ExitTransition.None
+ )
+ ExitToLeft.assertTransitions(EnterTransition.None, mockExitToLeftTransition)
+ ExitToRight.assertTransitions(EnterTransition.None, mockExitToRightTransition)
+ EnterWithExpand.assertTransitions(mockEnterWithExpandTransition, ExitTransition.None)
+ ExitWithShrink.assertTransitions(EnterTransition.None, mockExitWithShrinkTransition)
+ }
+
+ private fun DefaultPaneMotion.assertTransitions(
+ expectedEnterTransition: EnterTransition,
+ expectedExitTransition: ExitTransition
+ ) {
+ // Can't compare equality directly because of lambda. Check string representation instead
+ assertWithMessage("Enter transition of $this: ")
+ .that(mockPaneMotionScope.enterTransition.toString())
+ .isEqualTo(expectedEnterTransition.toString())
+ assertWithMessage("Exit transition of $this: ")
+ .that(mockPaneMotionScope.exitTransition.toString())
+ .isEqualTo(expectedExitTransition.toString())
+ }
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@@ -166,3 +216,84 @@
arrayOf(AnimateBounds, AnimateBounds, AnimateBounds), // To V, V, V
),
)
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private val mockPaneMotionScope =
+ object : PaneMotionScope {
+ override val positionAnimationSpec: FiniteAnimationSpec<IntOffset> = tween()
+ override val sizeAnimationSpec: FiniteAnimationSpec<IntSize> = spring()
+ override val delayedPositionAnimationSpec: FiniteAnimationSpec<IntOffset> = snap()
+ override val slideInFromLeftOffset: Int = 1
+ override val slideInFromRightOffset: Int = 2
+ override val slideOutToLeftOffset: Int = 3
+ override val slideOutToRightOffset: Int = 4
+ override val motionProgress: () -> Float = { 0.5F }
+ override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
+ get() = mockLayoutCoordinates
+
+ override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates =
+ mockLayoutCoordinates
+
+ val mockLayoutCoordinates =
+ object : LayoutCoordinates {
+ override val isAttached: Boolean = false
+ override val parentCoordinates: LayoutCoordinates? = null
+ override val parentLayoutCoordinates: LayoutCoordinates? = null
+ override val providedAlignmentLines: Set<AlignmentLine> = emptySet()
+ override val size: IntSize = IntSize.Zero
+
+ override fun get(alignmentLine: AlignmentLine): Int = 0
+
+ override fun localBoundingBoxOf(
+ sourceCoordinates: LayoutCoordinates,
+ clipBounds: Boolean
+ ): Rect = Rect.Zero
+
+ override fun localPositionOf(
+ sourceCoordinates: LayoutCoordinates,
+ relativeToSource: Offset
+ ): Offset = Offset.Zero
+
+ override fun localToRoot(relativeToLocal: Offset): Offset = Offset.Zero
+
+ override fun localToWindow(relativeToLocal: Offset): Offset = Offset.Zero
+
+ override fun windowToLocal(relativeToWindow: Offset): Offset = Offset.Zero
+ }
+ }
+
+private val mockEnterFromLeftTransition =
+ slideInHorizontally(mockPaneMotionScope.positionAnimationSpec) {
+ mockPaneMotionScope.slideInFromLeftOffset
+ }
+
+private val mockEnterFromRightTransition =
+ slideInHorizontally(mockPaneMotionScope.positionAnimationSpec) {
+ mockPaneMotionScope.slideInFromRightOffset
+ }
+
+private val mockEnterFromLeftDelayedTransition =
+ slideInHorizontally(mockPaneMotionScope.delayedPositionAnimationSpec) {
+ mockPaneMotionScope.slideInFromLeftOffset
+ }
+
+private val mockEnterFromRightDelayedTransition =
+ slideInHorizontally(mockPaneMotionScope.delayedPositionAnimationSpec) {
+ mockPaneMotionScope.slideInFromLeftOffset
+ }
+
+private val mockExitToLeftTransition =
+ slideOutHorizontally(mockPaneMotionScope.positionAnimationSpec) {
+ mockPaneMotionScope.slideOutToLeftOffset
+ }
+
+private val mockExitToRightTransition =
+ slideOutHorizontally(mockPaneMotionScope.positionAnimationSpec) {
+ mockPaneMotionScope.slideOutToRightOffset
+ }
+
+private val mockEnterWithExpandTransition =
+ expandHorizontally(mockPaneMotionScope.sizeAnimationSpec, Alignment.CenterHorizontally)
+
+private val mockExitWithShrinkTransition =
+ shrinkHorizontally(mockPaneMotionScope.sizeAnimationSpec, Alignment.CenterHorizontally)
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
index ecb54e7..e151d69 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
@@ -17,16 +17,39 @@
package androidx.compose.material3.adaptive.layout
import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.expandHorizontally
+import androidx.compose.animation.shrinkHorizontally
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
import kotlin.math.max
import kotlin.math.min
@ExperimentalMaterial3AdaptiveApi
+internal interface PaneMotionScope : LookaheadScope {
+ val positionAnimationSpec: FiniteAnimationSpec<IntOffset>
+ val sizeAnimationSpec: FiniteAnimationSpec<IntSize>
+ val delayedPositionAnimationSpec: FiniteAnimationSpec<IntOffset>
+ val slideInFromLeftOffset: Int
+ val slideInFromRightOffset: Int
+ val slideOutToLeftOffset: Int
+ val slideOutToRightOffset: Int
+ val motionProgress: () -> Float
+}
+
+@ExperimentalMaterial3AdaptiveApi
internal interface PaneMotion {
- // TODO (conradchen): Implement the following fields
- // val enterTransition: EnterTransition
- // val exitTransition: ExitTransition
- // val animateBoundsModifier: Modifier
+ val PaneMotionScope.enterTransition: EnterTransition
+ val PaneMotionScope.exitTransition: ExitTransition
+ val PaneMotionScope.animateBoundsModifier: Modifier
}
@ExperimentalMaterial3AdaptiveApi
@@ -41,22 +64,60 @@
val EnterFromRightDelayed = DefaultPaneMotion(5)
val ExitToLeft = DefaultPaneMotion(6)
val ExitToRight = DefaultPaneMotion(7)
- val ExitWithShrink = DefaultPaneMotion(8)
- val EnterWithExpand = DefaultPaneMotion(9)
+ val EnterWithExpand = DefaultPaneMotion(8)
+ val ExitWithShrink = DefaultPaneMotion(9)
}
+ override val PaneMotionScope.enterTransition: EnterTransition
+ get() =
+ when (this@DefaultPaneMotion) {
+ EnterFromLeft ->
+ slideInHorizontally(positionAnimationSpec) { slideInFromLeftOffset }
+ EnterFromRight ->
+ slideInHorizontally(positionAnimationSpec) { slideInFromRightOffset }
+ EnterFromLeftDelayed ->
+ slideInHorizontally(delayedPositionAnimationSpec) { slideInFromLeftOffset }
+ EnterFromRightDelayed ->
+ slideInHorizontally(delayedPositionAnimationSpec) { slideInFromRightOffset }
+ // TODO(conradche): Figure out how to expand with position change
+ EnterWithExpand ->
+ expandHorizontally(sizeAnimationSpec, Alignment.CenterHorizontally)
+ else -> EnterTransition.None
+ }
+
+ override val PaneMotionScope.exitTransition: ExitTransition
+ get() =
+ when (this@DefaultPaneMotion) {
+ ExitToLeft -> slideOutHorizontally(positionAnimationSpec) { slideOutToLeftOffset }
+ ExitToRight -> slideOutHorizontally(positionAnimationSpec) { slideOutToRightOffset }
+ // TODO(conradche): Figure out how to shrink with position change
+ ExitWithShrink ->
+ shrinkHorizontally(sizeAnimationSpec, Alignment.CenterHorizontally)
+ else -> ExitTransition.None
+ }
+
+ override val PaneMotionScope.animateBoundsModifier: Modifier
+ get() =
+ Modifier.animateBounds(
+ motionProgress,
+ sizeAnimationSpec,
+ positionAnimationSpec,
+ this,
+ this@DefaultPaneMotion == AnimateBounds
+ )
+
override fun toString(): String =
- when (value) {
- 0 -> "NoMotion"
- 1 -> "AnimateBounds"
- 2 -> "EnterFromLeft"
- 3 -> "EnterFromRight"
- 4 -> "EnterFromLeftDelayed"
- 5 -> "EnterFromRightDelayed"
- 6 -> "ExitToLeft"
- 7 -> "ExitToRight"
- 8 -> "ExitWithShrink"
- 9 -> "EnterWithExpand"
+ when (this) {
+ NoMotion -> "NoMotion"
+ AnimateBounds -> "AnimateBounds"
+ EnterFromLeft -> "EnterFromLeft"
+ EnterFromRight -> "EnterFromRight"
+ EnterFromLeftDelayed -> "EnterFromLeftDelayed"
+ EnterFromRightDelayed -> "EnterFromRightDelayed"
+ ExitToLeft -> "ExitToLeft"
+ ExitToRight -> "ExitToRight"
+ EnterWithExpand -> "EnterWithExpand"
+ ExitWithShrink -> "ExitWithShrink"
else -> "Undefined($value)"
}
}