Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a97625ec authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Introduce CustomPropertyTransformation

This CL introduces a new CustomPropertyTransformation to the DSL. This
allows users of STL to create custom transformations that are more
complex than the default transformations. The goal is to create custom
transformations using our Motion Mechanics to implement better Dual
Shade transitions. See go/stl-dual-shade for details.

At first these custom transformations will be limited to normal
transition specs, and we throw an exception when they are defined in a
preview spec or overscroll spec. We plan to rework overscroll and
previews in the coming weeks, at which point we will reconcile them with
custom transformations.

Bug: 376438969
Test: atest CustomTransformationTest
Test: atest SceneTransitionLayoutTest
Flag: com.android.systemui.scene_container
Change-Id: I91a27f82d458b2acaffb8c9cbd096dde9c2c04a2
parent 23669cd8
Loading
Loading
Loading
Loading
+66 −10
Original line number Diff line number Diff line
@@ -50,6 +50,8 @@ import androidx.compose.ui.util.fastForEachReversed
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 @@ private inline fun <T> computeValue(
                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 @@ private inline fun <T> computeValue(
    // 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 @@ private inline fun <T> computeValue(
        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 @@ private inline fun <T> computeValue(
            }

        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 @@ private inline fun <T> computeValue(
            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 @@ private inline fun <T> computeValue(
                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 @@ private inline fun <T> computeValue(

    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> computeValue(
    }
}

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,
+4 −3
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

/**
@@ -466,9 +467,9 @@ internal class MutableSceneTransitionLayoutStateImpl(
            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)) {
+16 −17
Original line number Diff line number Diff line
@@ -26,18 +26,18 @@ import androidx.compose.ui.geometry.Offset
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 @@ internal class TransformationSpecImpl(
                        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")
            }
        }

+11 −0
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import androidx.compose.ui.unit.Density
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 @@ interface PropertyTransformationBuilder {
        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. */
+16 −10
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import androidx.compose.ui.unit.Dp
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 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder {
        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 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder {
    }

    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 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder {
        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 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder {
        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 @@ internal class TransitionBuilderImpl(override val transition: TransitionState.Tr
                "(${transition.toContent.debugName})"
        }

        transformation(SharedElementTransformation(matcher, enabled, elevateInContent))
        addTransformation(SharedElementTransformation(matcher, enabled, elevateInContent))
    }

    override fun timestampRange(
@@ -288,6 +294,6 @@ internal open class OverscrollBuilderImpl : BaseTransitionBuilderImpl(), Overscr
        x: OverscrollScope.() -> Float,
        y: OverscrollScope.() -> Float,
    ) {
        transformation(OverscrollTranslate(matcher, x, y))
        addTransformation(OverscrollTranslate(matcher, x, y))
    }
}
Loading