Merge changes I91a27f82,I62688e42 into main
* changes:
Introduce CustomPropertyTransformation
Remove Transformation.reversed()
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 d976e8e..44f60cb 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
@@ -50,6 +50,8 @@
import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
+import com.android.compose.animation.scene.transformation.InterpolatedPropertyTransformation
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.animation.scene.transformation.TransformationWithRange
@@ -1308,7 +1310,14 @@
checkNotNull(if (currentContent == toContent) toState else fromState)
val idleValue = contentValue(overscrollState)
val targetValue =
- with(propertySpec.transformation) {
+ with(
+ propertySpec.transformation.requireInterpolatedTransformation(
+ element,
+ transition,
+ ) {
+ "Custom transformations in overscroll specs should not be possible"
+ }
+ ) {
layoutImpl.propertyTransformationScope.transform(
currentContent,
element.key,
@@ -1390,7 +1399,7 @@
// fromContent or toContent during interruptions.
val content = contentState.content
- val transformation =
+ val transformationWithRange =
transformation(transition.transformationSpec.transformations(element.key, content))
val previewTransformation =
@@ -1403,7 +1412,14 @@
val idleValue = contentValue(contentState)
val isEntering = content == toContent
val previewTargetValue =
- with(previewTransformation.transformation) {
+ with(
+ previewTransformation.transformation.requireInterpolatedTransformation(
+ element,
+ transition,
+ ) {
+ "Custom transformations in preview specs should not be possible"
+ }
+ ) {
layoutImpl.propertyTransformationScope.transform(
content,
element.key,
@@ -1413,8 +1429,15 @@
}
val targetValueOrNull =
- transformation?.let { transformation ->
- with(transformation.transformation) {
+ transformationWithRange?.let { transformation ->
+ with(
+ transformation.transformation.requireInterpolatedTransformation(
+ element,
+ transition,
+ ) {
+ "Custom transformations are not allowed for properties with a preview"
+ }
+ ) {
layoutImpl.propertyTransformationScope.transform(
content,
element.key,
@@ -1461,7 +1484,7 @@
lerp(
lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress),
idleValue,
- transformation?.range?.progress(transition.progress) ?: transition.progress,
+ transformationWithRange?.range?.progress(transition.progress) ?: transition.progress,
)
} else {
if (targetValueOrNull == null) {
@@ -1474,22 +1497,39 @@
lerp(
lerp(idleValue, previewTargetValue, previewRangeProgress),
targetValueOrNull,
- transformation.range?.progress(transition.progress) ?: transition.progress,
+ transformationWithRange.range?.progress(transition.progress)
+ ?: transition.progress,
)
}
}
}
- if (transformation == null) {
+ if (transformationWithRange == null) {
// If there is no transformation explicitly associated to this element value, let's use
// the value given by the system (like the current position and size given by the layout
// pass).
return currentValue()
}
+ val transformation = transformationWithRange.transformation
+ when (transformation) {
+ is CustomPropertyTransformation ->
+ return with(transformation) {
+ layoutImpl.propertyTransformationScope.transform(
+ content,
+ element.key,
+ transition,
+ transition.coroutineScope,
+ )
+ }
+ is InterpolatedPropertyTransformation -> {
+ /* continue */
+ }
+ }
+
val idleValue = contentValue(contentState)
val targetValue =
- with(transformation.transformation) {
+ with(transformation) {
layoutImpl.propertyTransformationScope.transform(
content,
element.key,
@@ -1506,7 +1546,7 @@
val progress = transition.progress
// TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
- val rangeProgress = transformation.range?.progress(progress) ?: progress
+ val rangeProgress = transformationWithRange.range?.progress(progress) ?: progress
// Interpolate between the value at rest and the value before entering/after leaving.
val isEntering =
@@ -1523,6 +1563,22 @@
}
}
+private inline fun <T> PropertyTransformation<T>.requireInterpolatedTransformation(
+ element: Element,
+ transition: TransitionState.Transition,
+ errorMessage: () -> String,
+): InterpolatedPropertyTransformation<T> {
+ return when (this) {
+ is InterpolatedPropertyTransformation -> this
+ is CustomPropertyTransformation -> {
+ val elem = element.key.debugName
+ val fromContent = transition.fromContent
+ val toContent = transition.toContent
+ error("${errorMessage()} (element=$elem fromContent=$fromContent toContent=$toContent)")
+ }
+ }
+}
+
private inline fun <T> interpolateSharedElement(
transition: TransitionState.Transition,
contentValue: (Element.State) -> T,
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 72b29ee..3c01dfe 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
@@ -32,6 +32,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
/**
@@ -466,9 +467,9 @@
return
}
- // Make sure that this transition settles in case it was force finished, for instance by
- // calling snapToScene().
- transition.freezeAndAnimateToCurrentState()
+ // Make sure that this transition is cancelled in case it was force finished, for instance
+ // if snapToScene() is called.
+ transition.coroutineScope.cancel()
val transitionStates = this.transitionStates
if (!transitionStates.contains(transition)) {
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 b083f79..569593c 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
@@ -26,18 +26,18 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
import com.android.compose.animation.scene.content.state.TransitionState
-import com.android.compose.animation.scene.transformation.AnchoredSize
-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.CustomAlphaTransformation
+import com.android.compose.animation.scene.transformation.CustomOffsetTransformation
+import com.android.compose.animation.scene.transformation.CustomScaleTransformation
+import com.android.compose.animation.scene.transformation.CustomSizeTransformation
+import com.android.compose.animation.scene.transformation.InterpolatedAlphaTransformation
+import com.android.compose.animation.scene.transformation.InterpolatedOffsetTransformation
+import com.android.compose.animation.scene.transformation.InterpolatedScaleTransformation
+import com.android.compose.animation.scene.transformation.InterpolatedSizeTransformation
import com.android.compose.animation.scene.transformation.PropertyTransformation
-import com.android.compose.animation.scene.transformation.ScaleSize
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.animation.scene.transformation.Transformation
import com.android.compose.animation.scene.transformation.TransformationWithRange
-import com.android.compose.animation.scene.transformation.Translate
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions
@@ -359,35 +359,34 @@
transformationWithRange
as TransformationWithRange<SharedElementTransformation>
}
- is Translate,
- is OverscrollTranslate,
- is EdgeTranslate,
- is AnchoredTranslate -> {
+ is InterpolatedOffsetTransformation,
+ is CustomOffsetTransformation -> {
throwIfNotNull(offset, element, name = "offset")
offset =
transformationWithRange
as TransformationWithRange<PropertyTransformation<Offset>>
}
- is ScaleSize,
- is AnchoredSize -> {
+ is InterpolatedSizeTransformation,
+ is CustomSizeTransformation -> {
throwIfNotNull(size, element, name = "size")
size =
transformationWithRange
as TransformationWithRange<PropertyTransformation<IntSize>>
}
- is DrawScale -> {
+ is InterpolatedScaleTransformation,
+ is CustomScaleTransformation -> {
throwIfNotNull(drawScale, element, name = "drawScale")
drawScale =
transformationWithRange
as TransformationWithRange<PropertyTransformation<Scale>>
}
- is Fade -> {
+ is InterpolatedAlphaTransformation,
+ is CustomAlphaTransformation -> {
throwIfNotNull(alpha, element, name = "alpha")
alpha =
transformationWithRange
as TransformationWithRange<PropertyTransformation<Float>>
}
- else -> error("Unknown transformation: $transformation")
}
}
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 dc26b6b..1fdfca9 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
@@ -26,6 +26,7 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
import kotlin.math.tanh
/** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
@@ -527,6 +528,16 @@
anchorWidth: Boolean = true,
anchorHeight: Boolean = true,
)
+
+ /**
+ * Apply a [CustomPropertyTransformation] to one or more elements.
+ *
+ * @see com.android.compose.animation.scene.transformation.CustomSizeTransformation
+ * @see com.android.compose.animation.scene.transformation.CustomOffsetTransformation
+ * @see com.android.compose.animation.scene.transformation.CustomAlphaTransformation
+ * @see com.android.compose.animation.scene.transformation.CustomScaleTransformation
+ */
+ fun transformation(transformation: CustomPropertyTransformation<*>)
}
/** This converter lets you change a linear progress into a function of your choice. */
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 e461f9c..79f8cd4 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
@@ -30,6 +30,7 @@
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.AnchoredSize
import com.android.compose.animation.scene.transformation.AnchoredTranslate
+import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
import com.android.compose.animation.scene.transformation.DrawScale
import com.android.compose.animation.scene.transformation.EdgeTranslate
import com.android.compose.animation.scene.transformation.Fade
@@ -173,7 +174,7 @@
range = null
}
- protected fun transformation(transformation: Transformation) {
+ protected fun addTransformation(transformation: Transformation) {
val transformationWithRange = TransformationWithRange(transformation, range)
transformations.add(
if (reversed) {
@@ -185,11 +186,11 @@
}
override fun fade(matcher: ElementMatcher) {
- transformation(Fade(matcher))
+ addTransformation(Fade(matcher))
}
override fun translate(matcher: ElementMatcher, x: Dp, y: Dp) {
- transformation(Translate(matcher, x, y))
+ addTransformation(Translate(matcher, x, y))
}
override fun translate(
@@ -197,19 +198,19 @@
edge: Edge,
startsOutsideLayoutBounds: Boolean,
) {
- transformation(EdgeTranslate(matcher, edge, startsOutsideLayoutBounds))
+ addTransformation(EdgeTranslate(matcher, edge, startsOutsideLayoutBounds))
}
override fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey) {
- transformation(AnchoredTranslate(matcher, anchor))
+ addTransformation(AnchoredTranslate(matcher, anchor))
}
override fun scaleSize(matcher: ElementMatcher, width: Float, height: Float) {
- transformation(ScaleSize(matcher, width, height))
+ addTransformation(ScaleSize(matcher, width, height))
}
override fun scaleDraw(matcher: ElementMatcher, scaleX: Float, scaleY: Float, pivot: Offset) {
- transformation(DrawScale(matcher, scaleX, scaleY, pivot))
+ addTransformation(DrawScale(matcher, scaleX, scaleY, pivot))
}
override fun anchoredSize(
@@ -218,7 +219,12 @@
anchorWidth: Boolean,
anchorHeight: Boolean,
) {
- transformation(AnchoredSize(matcher, anchor, anchorWidth, anchorHeight))
+ addTransformation(AnchoredSize(matcher, anchor, anchorWidth, anchorHeight))
+ }
+
+ override fun transformation(transformation: CustomPropertyTransformation<*>) {
+ check(range == null) { "Custom transformations can not be applied inside a range" }
+ addTransformation(transformation)
}
}
@@ -257,7 +263,7 @@
"(${transition.toContent.debugName})"
}
- transformation(SharedElementTransformation(matcher, enabled, elevateInContent))
+ addTransformation(SharedElementTransformation(matcher, enabled, elevateInContent))
}
override fun timestampRange(
@@ -288,6 +294,6 @@
x: OverscrollScope.() -> Float,
y: OverscrollScope.() -> Float,
) {
- transformation(OverscrollTranslate(matcher, x, y))
+ addTransformation(OverscrollTranslate(matcher, x, y))
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index e3118d67..75584ba 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -35,6 +35,8 @@
import com.android.compose.animation.scene.TransformationSpec
import com.android.compose.animation.scene.TransformationSpecImpl
import com.android.compose.animation.scene.TransitionKey
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/** The state associated to a [SceneTransitionLayout] at some specific point in time. */
@@ -279,8 +281,24 @@
*/
private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null
- /** Whether this transition was already started. */
- private var wasStarted = false
+ /**
+ * The coroutine scope associated to this transition.
+ *
+ * This coroutine scope can be used to launch animations associated to this transition,
+ * which will not finish until at least one animation/job is still running in the scope.
+ *
+ * Important: Make sure to never launch long-running jobs in this scope, otherwise the
+ * transition will never be considered as finished.
+ */
+ internal val coroutineScope: CoroutineScope
+ get() =
+ _coroutineScope
+ ?: error(
+ "Transition.coroutineScope can only be accessed once the transition was " +
+ "started "
+ )
+
+ private var _coroutineScope: CoroutineScope? = null
init {
check(fromContent != toContent)
@@ -341,10 +359,11 @@
abstract fun freezeAndAnimateToCurrentState()
internal suspend fun runInternal() {
- check(!wasStarted) { "A Transition can be started only once." }
- wasStarted = true
-
- run()
+ check(_coroutineScope == null) { "A Transition can be started only once." }
+ coroutineScope {
+ _coroutineScope = this
+ run()
+ }
}
internal fun updateOverscrollSpecs(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index 0ddeb7c..85bb533 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -28,7 +28,7 @@
private val anchor: ElementKey,
private val anchorWidth: Boolean,
private val anchorHeight: Boolean,
-) : PropertyTransformation<IntSize> {
+) : InterpolatedSizeTransformation {
override fun PropertyTransformationScope.transform(
content: ContentKey,
element: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 47508b4..04cd683 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -26,7 +26,7 @@
internal class AnchoredTranslate(
override val matcher: ElementMatcher,
private val anchor: ElementKey,
-) : PropertyTransformation<Offset> {
+) : InterpolatedOffsetTransformation {
override fun PropertyTransformationScope.transform(
content: ContentKey,
element: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index 8488ae5..45d6d40 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -32,7 +32,7 @@
private val scaleX: Float,
private val scaleY: Float,
private val pivot: Offset = Offset.Unspecified,
-) : PropertyTransformation<Scale> {
+) : InterpolatedScaleTransformation {
override fun PropertyTransformationScope.transform(
content: ContentKey,
element: ElementKey,
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 884aae4b..21d66d7 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
@@ -28,7 +28,7 @@
override val matcher: ElementMatcher,
private val edge: Edge,
private val startsOutsideLayoutBounds: Boolean = true,
-) : PropertyTransformation<Offset> {
+) : InterpolatedOffsetTransformation {
override fun PropertyTransformationScope.transform(
content: ContentKey,
element: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index ef769e7..d942273 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -22,7 +22,7 @@
import com.android.compose.animation.scene.content.state.TransitionState
/** Fade an element in or out. */
-internal class Fade(override val matcher: ElementMatcher) : PropertyTransformation<Float> {
+internal class Fade(override val matcher: ElementMatcher) : InterpolatedAlphaTransformation {
override fun PropertyTransformationScope.transform(
content: ContentKey,
element: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index ef3654b..5f3cdab 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -31,7 +31,7 @@
override val matcher: ElementMatcher,
private val width: Float = 1f,
private val height: Float = 1f,
-) : PropertyTransformation<IntSize> {
+) : InterpolatedSizeTransformation {
override fun PropertyTransformationScope.transform(
content: ContentKey,
element: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 74a3ead..d5143d7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -18,7 +18,9 @@
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.LinearEasing
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceAtMost
@@ -27,7 +29,9 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.ElementStateScope
+import com.android.compose.animation.scene.Scale
import com.android.compose.animation.scene.content.state.TransitionState
+import kotlinx.coroutines.CoroutineScope
/** A transformation applied to one or more elements during a transition. */
sealed interface Transformation {
@@ -35,12 +39,6 @@
* The matcher that should match the element(s) to which this transformation should be applied.
*/
val matcher: ElementMatcher
-
- /*
- * Reverse this transformation. This is called when we use Transition(from = A, to = B) when
- * animating from B to A and there is no Transition(from = B, to = A) defined.
- */
- fun reversed(): Transformation = this
}
internal class SharedElementTransformation(
@@ -50,7 +48,13 @@
) : Transformation
/** A transformation that changes the value of an element property, like its size or offset. */
-interface PropertyTransformation<T> : Transformation {
+sealed interface PropertyTransformation<T> : Transformation
+
+/**
+ * A transformation to a target/transformed value that is automatically interpolated using the
+ * transition progress and transformation range.
+ */
+sealed interface InterpolatedPropertyTransformation<T> : PropertyTransformation<T> {
/**
* Return the transformed value for the given property, i.e.:
* - the value at progress = 0% for elements that are entering the layout (i.e. elements in the
@@ -58,8 +62,8 @@
* - the value at progress = 100% for elements that are leaving the layout (i.e. elements in the
* content we are transitioning from).
*
- * The returned value will be interpolated using the [transition] progress and [idleValue], the
- * value of the property when we are idle.
+ * The returned value will be automatically interpolated using the [transition] progress, the
+ * transformation range and [idleValue], the value of the property when we are idle.
*/
fun PropertyTransformationScope.transform(
content: ContentKey,
@@ -69,6 +73,50 @@
): T
}
+/** An [InterpolatedPropertyTransformation] applied to the size of one or more elements. */
+interface InterpolatedSizeTransformation : InterpolatedPropertyTransformation<IntSize>
+
+/** An [InterpolatedPropertyTransformation] applied to the offset of one or more elements. */
+interface InterpolatedOffsetTransformation : InterpolatedPropertyTransformation<Offset>
+
+/** An [InterpolatedPropertyTransformation] applied to the alpha of one or more elements. */
+interface InterpolatedAlphaTransformation : InterpolatedPropertyTransformation<Float>
+
+/** An [InterpolatedPropertyTransformation] applied to the scale of one or more elements. */
+interface InterpolatedScaleTransformation : InterpolatedPropertyTransformation<Scale>
+
+sealed interface CustomPropertyTransformation<T> : PropertyTransformation<T> {
+ /**
+ * Return the value that the property should have in the current frame for the given [content]
+ * and [element].
+ *
+ * This transformation can use [transitionScope] to launch animations associated to
+ * [transition], which will not finish until at least one animation/job is still running in the
+ * scope.
+ *
+ * Important: Make sure to never launch long-running jobs in [transitionScope], otherwise
+ * [transition] will never be considered as finished.
+ */
+ fun PropertyTransformationScope.transform(
+ content: ContentKey,
+ element: ElementKey,
+ transition: TransitionState.Transition,
+ transitionScope: CoroutineScope,
+ ): T
+}
+
+/** A [CustomPropertyTransformation] applied to the size of one or more elements. */
+interface CustomSizeTransformation : CustomPropertyTransformation<IntSize>
+
+/** A [CustomPropertyTransformation] applied to the offset of one or more elements. */
+interface CustomOffsetTransformation : CustomPropertyTransformation<Offset>
+
+/** A [CustomPropertyTransformation] applied to the alpha of one or more elements. */
+interface CustomAlphaTransformation : CustomPropertyTransformation<Float>
+
+/** A [CustomPropertyTransformation] applied to the scale of one or more elements. */
+interface CustomScaleTransformation : CustomPropertyTransformation<Scale>
+
interface PropertyTransformationScope : Density, ElementStateScope {
/** The current [direction][LayoutDirection] of the layout. */
val layoutDirection: LayoutDirection
@@ -101,7 +149,7 @@
}
/** Reverse this range. */
- fun reversed() =
+ internal fun reversed() =
TransformationRange(start = reverseBound(end), end = reverseBound(start), easing = easing)
/** Get the progress of this range given the global [transitionProgress]. */
@@ -128,6 +176,6 @@
}
companion object {
- const val BoundUnspecified = Float.MIN_VALUE
+ internal const val BoundUnspecified = Float.MIN_VALUE
}
}
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 356ed99..d756c86 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
@@ -30,7 +30,7 @@
override val matcher: ElementMatcher,
private val x: Dp = 0.dp,
private val y: Dp = 0.dp,
-) : PropertyTransformation<Offset> {
+) : InterpolatedOffsetTransformation {
override fun PropertyTransformationScope.transform(
content: ContentKey,
element: ElementKey,
@@ -45,7 +45,7 @@
override val matcher: ElementMatcher,
val x: OverscrollScope.() -> Float = { 0f },
val y: OverscrollScope.() -> Float = { 0f },
-) : PropertyTransformation<Offset> {
+) : InterpolatedOffsetTransformation {
private val cachedOverscrollScope = CachedOverscrollScope()
override fun PropertyTransformationScope.transform(
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 2b70908..ce1c8f8 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -35,12 +35,15 @@
import com.android.compose.test.transition
import com.google.common.truth.Truth.assertThat
import kotlin.coroutines.cancellation.CancellationException
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Rule
@@ -601,4 +604,47 @@
runBlocking { state.startTransition(transition) }
}
}
+
+ @Test
+ fun transitionFinishedWhenScopeIsEmpty() = runTest {
+ val state = MutableSceneTransitionLayoutState(SceneA)
+
+ // Start a transition.
+ val transition = transition(from = SceneA, to = SceneB)
+ state.startTransitionImmediately(backgroundScope, transition)
+ assertThat(state.transitionState).isSceneTransition()
+
+ // Start a job in the transition scope.
+ val jobCompletable = CompletableDeferred<Unit>()
+ transition.coroutineScope.launch { jobCompletable.await() }
+
+ // Finish the transition (i.e. make its #run() method return). The transition should not be
+ // considered as finished yet given that there is a job still running in its scope.
+ transition.finish()
+ runCurrent()
+ assertThat(state.transitionState).isSceneTransition()
+
+ // Finish the job in the scope. Now the transition should be considered as finished.
+ jobCompletable.complete(Unit)
+ runCurrent()
+ assertThat(state.transitionState).isIdle()
+ }
+
+ @Test
+ fun transitionScopeIsCancelledWhenTransitionIsForceFinished() = runTest {
+ val state = MutableSceneTransitionLayoutState(SceneA)
+
+ // Start a transition.
+ val transition = transition(from = SceneA, to = SceneB)
+ state.startTransitionImmediately(backgroundScope, transition)
+ assertThat(state.transitionState).isSceneTransition()
+
+ // Start a job in the transition scope that never finishes.
+ val job = transition.coroutineScope.launch { awaitCancellation() }
+
+ // Force snap state to SceneB to force finish all current transitions.
+ state.snapToScene(SceneB)
+ assertThat(state.transitionState).isIdle()
+ assertThat(job.isCancelled).isTrue()
+ }
}
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 d317114..1f9ba9e 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,17 +22,22 @@
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.transformation.CustomSizeTransformation
import com.android.compose.animation.scene.transformation.OverscrollTranslate
+import com.android.compose.animation.scene.transformation.PropertyTransformationScope
import com.android.compose.animation.scene.transformation.TransformationRange
import com.android.compose.animation.scene.transformation.TransformationWithRange
import com.android.compose.test.transition
import com.google.common.truth.Correspondence
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Test
@@ -343,6 +348,33 @@
assertThat(transitionPassedToBuilder).isSameInstanceAs(transition)
}
+ @Test
+ fun customTransitionsAreNotSupportedInRanges() = runTest {
+ val transitions = transitions {
+ from(SceneA, to = SceneB) {
+ fractionRange {
+ transformation(
+ object : CustomSizeTransformation {
+ override val matcher: ElementMatcher = TestElements.Foo
+
+ override fun PropertyTransformationScope.transform(
+ content: ContentKey,
+ element: ElementKey,
+ transition: TransitionState.Transition,
+ transitionScope: CoroutineScope,
+ ): IntSize = IntSize.Zero
+ }
+ )
+ }
+ }
+ }
+
+ val state = MutableSceneTransitionLayoutState(SceneA, transitions)
+ assertThrows(IllegalStateException::class.java) {
+ runBlocking { state.startTransition(transition(from = SceneA, to = SceneB)) }
+ }
+ }
+
companion object {
private val TRANSFORMATION_RANGE =
Correspondence.transforming<TransformationWithRange<*>, TransformationRange?>(
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt
new file mode 100644
index 0000000..487b099
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import kotlinx.coroutines.CoroutineScope
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CustomTransformationTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun customSize() {
+ /** A size transformation that adds [add] to the size of the transformed element(s). */
+ class AddSizeTransformation(override val matcher: ElementMatcher, private val add: Dp) :
+ CustomSizeTransformation {
+ override fun PropertyTransformationScope.transform(
+ content: ContentKey,
+ element: ElementKey,
+ transition: TransitionState.Transition,
+ transitionScope: CoroutineScope,
+ ): IntSize {
+ val idleSize = checkNotNull(element.targetSize(content))
+ val progress = 1f - transition.progressTo(content)
+ val addPx = (add * progress).roundToPx()
+ return IntSize(width = idleSize.width + addPx, height = idleSize.height + addPx)
+ }
+ }
+
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.element(TestElements.Foo).size(40.dp, 20.dp)) },
+ toSceneContent = {},
+ transition = {
+ spec = tween(16 * 4, easing = LinearEasing)
+
+ // Add 80dp to the width and height of Foo.
+ transformation(AddSizeTransformation(TestElements.Foo, 80.dp))
+ },
+ ) {
+ before { onElement(TestElements.Foo).assertSizeIsEqualTo(40.dp, 20.dp) }
+ at(0) { onElement(TestElements.Foo).assertSizeIsEqualTo(40.dp, 20.dp) }
+ at(16) { onElement(TestElements.Foo).assertSizeIsEqualTo(60.dp, 40.dp) }
+ at(32) { onElement(TestElements.Foo).assertSizeIsEqualTo(80.dp, 60.dp) }
+ at(48) { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 80.dp) }
+ after { onElement(TestElements.Foo).assertDoesNotExist() }
+ }
+ }
+
+ @Test
+ fun customOffset() {
+ /** An offset transformation that adds [add] to the offset of the transformed element(s). */
+ class AddOffsetTransformation(override val matcher: ElementMatcher, private val add: Dp) :
+ CustomOffsetTransformation {
+ override fun PropertyTransformationScope.transform(
+ content: ContentKey,
+ element: ElementKey,
+ transition: TransitionState.Transition,
+ transitionScope: CoroutineScope,
+ ): Offset {
+ val idleOffset = checkNotNull(element.targetOffset(content))
+ val progress = 1f - transition.progressTo(content)
+ val addPx = (add * progress).toPx()
+ return Offset(x = idleOffset.x + addPx, y = idleOffset.y + addPx)
+ }
+ }
+
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.element(TestElements.Foo)) },
+ toSceneContent = {},
+ transition = {
+ spec = tween(16 * 4, easing = LinearEasing)
+
+ // Add 80dp to the offset of Foo.
+ transformation(AddOffsetTransformation(TestElements.Foo, 80.dp))
+ },
+ ) {
+ before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 0.dp) }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 0.dp) }
+ at(16) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(20.dp, 20.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(40.dp, 40.dp) }
+ at(48) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(60.dp, 60.dp) }
+ after { onElement(TestElements.Foo).assertDoesNotExist() }
+ }
+ }
+}