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

Commit 30211835 authored by Omar Miatello's avatar Omar Miatello
Browse files

feat(MM): Make MotionSpec declarative and state-driven (2/3)

This change refactors `MotionValue` to make its `MotionSpec` (animation
physics) derived from a reactive lambda, replacing the previous
imperative approach.

Previously, `MotionValue` had a mutable `spec` property that consumers
would update directly. This required imperative logic within consumers
(`motionValue.spec = ...`) which did not align well with Compose's
declarative, state-driven architecture.

The `MotionValue` constructor now accepts a `spec: () -> MotionSpec`
lambda. The value now automatically reacts to changes in any state read
within this lambda, making its behavior inherently declarative.

Test: Tested on the previous MotionValueTests
Bug: 428886057
Flag: com.android.systemui.scene_container
Change-Id: I8f1b29da698d6599edcd61d36108f005d21e706e
parent b9eba49b
Loading
Loading
Loading
Loading
+32 −14
Original line number Original line Diff line number Diff line
@@ -17,6 +17,10 @@
package com.android.compose.gesture
package com.android.compose.gesture


import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.PointerType
@@ -106,11 +110,34 @@ private class OverscrollToDismissNode(
    private val gestureContext =
    private val gestureContext =
        DistanceGestureContext(0f, InputDirection.Max, directionChangeSlop = 1f)
        DistanceGestureContext(0f, InputDirection.Max, directionChangeSlop = 1f)


    private var dragState: DragState by mutableStateOf(DragState.Idle)

    enum class DragState {
        Idle,
        Dragging,
        Dismissed,
    }

    private val spec = derivedStateOf {
        with(motionBuilderContext) {
            when (dragState) {
                DragState.Idle -> fixedSpatialValueSpec(0f, SnapBackSpring)
                DragState.Dragging -> spatialMotionSpec { after(0f, MagneticDetach()) }
                DragState.Dismissed ->
                    fixedSpatialValueSpec(
                        contentBoxWidth.toFloat(),
                        SnapBackSpring,
                        listOf(isDismissedState with true),
                    )
            }
        }
    }

    private val motionValue =
    private val motionValue =
        MotionValue(
        MotionValue(
            gestureContext::dragOffset,
            input = { gestureContext.dragOffset },
            gestureContext,
            gestureContext = gestureContext,
            motionBuilderContext.fixedSpatialValueSpec(0f),
            spec = spec::value,
        )
        )


    private var delegateNode =
    private var delegateNode =
@@ -161,7 +188,7 @@ private class OverscrollToDismissNode(
    ): NestedDraggable.Controller {
    ): NestedDraggable.Controller {
        overscrollSign = sign
        overscrollSign = sign
        gestureContext.reset(dragOffset = motionValue.output, direction = InputDirection.Max)
        gestureContext.reset(dragOffset = motionValue.output, direction = InputDirection.Max)
        motionValue.spec = motionBuilderContext.spatialMotionSpec { after(0f, MagneticDetach()) }
        dragState = DragState.Dragging


        return this
        return this
    }
    }
@@ -187,16 +214,7 @@ private class OverscrollToDismissNode(
                currentState == MagneticDetach.State.Attached ||
                currentState == MagneticDetach.State.Attached ||
                    (currentState == MagneticDetach.State.Detached && isFlingInOppositeDirection)
                    (currentState == MagneticDetach.State.Detached && isFlingInOppositeDirection)


            motionValue.spec =
            dragState = if (settleAttached) DragState.Idle else DragState.Dismissed
                if (settleAttached) {
                    motionBuilderContext.fixedSpatialValueSpec(0f, SnapBackSpring)
                } else {
                    motionBuilderContext.fixedSpatialValueSpec(
                        contentBoxWidth.toFloat(),
                        SnapBackSpring,
                        listOf(isDismissedState with true),
                    )
                }
        }
        }
        return velocity
        return velocity
    }
    }
+21 −9
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene.mechanics
package com.android.compose.animation.scene.mechanics


import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableFloatStateOf
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementKey
@@ -51,11 +52,13 @@ internal class TransitionScopedMechanicsAdapter(
    private val computeInput: MotionValueInput = { progress, _, _ -> progress },
    private val computeInput: MotionValueInput = { progress, _, _ -> progress },
    private val stableThreshold: Float = MotionValue.StableThresholdEffect,
    private val stableThreshold: Float = MotionValue.StableThresholdEffect,
    private val label: String? = null,
    private val label: String? = null,
    private val createSpec: SpecFactory,
    private val getSpec: SpecFactory,
) {
) {


    private val input = mutableFloatStateOf(0f)
    private val input = mutableFloatStateOf(0f)
    private var motionValue: MotionValue? = null
    private var motionValue: MotionValue? = null
    private var transformationContent: ContentKey? = null
    private var transformationElement: ElementKey? = null


    fun PropertyTransformationScope.update(
    fun PropertyTransformationScope.update(
        content: ContentKey,
        content: ContentKey,
@@ -68,23 +71,32 @@ internal class TransitionScopedMechanicsAdapter(
        var motionValue = motionValue
        var motionValue = motionValue


        if (motionValue == null) {
        if (motionValue == null) {
            transformationContent = content
            transformationElement = element
            motionValue =
            motionValue =
                MotionValue(
                MotionValue(
                    input::floatValue,
                    input = { input.floatValue },
                    gestureContext =
                        transition.gestureContext
                        transition.gestureContext
                            ?: ProvidedGestureContext(
                            ?: ProvidedGestureContext(
                                0f,
                                0f,
                                appearDirection(content, element, transition),
                                appearDirection(content, element, transition),
                            ),
                            ),
                    createSpec(content, element),
                    spec = derivedStateOf { getSpec(content, element) }::value,
                    stableThreshold = stableThreshold,
                    label = label,
                    label = label,
                    stableThreshold = stableThreshold,
                )
                )

            this@TransitionScopedMechanicsAdapter.motionValue = motionValue
            this@TransitionScopedMechanicsAdapter.motionValue = motionValue


            transitionScope.launch {
            transitionScope.launch {
                motionValue.keepRunningWhile { !transition.isProgressStable || !isStable }
                motionValue.keepRunningWhile { !transition.isProgressStable || !isStable }
            }
            }
        } else {
            check(content == transformationContent && element == transformationElement) {
                "update received ($content, $element), " +
                    "instead of ($transformationContent, $transformationElement)"
            }
        }
        }


        return motionValue.output
        return motionValue.output
+1 −1
Original line number Original line Diff line number Diff line
@@ -447,7 +447,7 @@ class TransitionScopedMechanicsAdapterTest {
        override val property = PropertyTransformation.Property.Offset
        override val property = PropertyTransformation.Property.Offset


        val motionValue =
        val motionValue =
            TransitionScopedMechanicsAdapter(createSpec = specFactory, stableThreshold = 1f)
            TransitionScopedMechanicsAdapter(getSpec = specFactory, stableThreshold = 1f)


        override fun PropertyTransformationScope.transform(
        override fun PropertyTransformationScope.transform(
            content: ContentKey,
            content: ContentKey,