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

Commit db566112 authored by Mike Schneider's avatar Mike Schneider
Browse files

Add adapter for managing lifecycle of MotionValues during a transition

Bug: 392534646
Test: TransitionScopedMechanicsAdapterTest
Flag: com.android.systemui.scene_container
Change-Id: I9307efa532076875b15781e57b2fee5c941d1fac
parent 4854d8f9
Loading
Loading
Loading
Loading
+16 −1
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.OverlayKey
@@ -241,6 +243,15 @@ sealed interface TransitionState {
        /** Additional gesture context whenever the transition is driven by a user gesture. */
        abstract val gestureContext: GestureContext?

        /**
         * True when the transition reached the end and the progress won't be updated anymore.
         *
         * [isProgressStable] will be `true` before this [Transition] is completed while there are
         * still custom transition animations settling.
         */
        var isProgressStable: Boolean by mutableStateOf(false)
            private set

        /** The CUJ covered by this transition. */
        @CujType
        val cuj: Int?
@@ -372,7 +383,11 @@ sealed interface TransitionState {
            check(_coroutineScope == null) { "A Transition can be started only once." }
            coroutineScope {
                _coroutineScope = this
                try {
                    run()
                } finally {
                    isProgressStable = true
                }
            }
        }

+135 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.mechanics

import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.mutableFloatStateOf
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementStateScope
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
import com.android.compose.animation.scene.transformation.PropertyTransformationScope
import com.android.mechanics.MotionValue
import com.android.mechanics.ProvidedGestureContext
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.MotionSpec
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
 * Callback to create a [MotionSpec] on the first call to [CustomPropertyTransformation.transform]
 */
typealias SpecFactory =
    PropertyTransformationScope.(content: ContentKey, element: ElementKey) -> MotionSpec

/** Callback to compute the [MotionValue] per frame */
typealias MotionValueInput =
    PropertyTransformationScope.(progress: Float, content: ContentKey, element: ElementKey) -> Float

/**
 * Adapter to create a [MotionValue] and `keepRunning()` it temporarily while a
 * [CustomPropertyTransformation] is in progress and until the animation settles.
 *
 * The [MotionValue]'s input is by default the transition progress.
 */
internal class TransitionScopedMechanicsAdapter(
    private val computeInput: MotionValueInput = { progress, _, _ -> progress },
    private val stableThreshold: Float = MotionValue.StableThresholdEffect,
    private val label: String? = null,
    private val createSpec: SpecFactory,
) {

    private val input = mutableFloatStateOf(0f)
    private var motionValue: MotionValue? = null

    fun PropertyTransformationScope.update(
        content: ContentKey,
        element: ElementKey,
        transition: TransitionState.Transition,
        transitionScope: CoroutineScope,
    ): Float {
        val progress = transition.progressTo(content)
        input.floatValue = computeInput(progress, content, element)
        var motionValue = motionValue

        if (motionValue == null) {
            motionValue =
                MotionValue(
                    input::floatValue,
                    transition.gestureContext
                        ?: ProvidedGestureContext(
                            0f,
                            appearDirection(content, element, transition),
                        ),
                    createSpec(content, element),
                    stableThreshold = stableThreshold,
                    label = label,
                )
            this@TransitionScopedMechanicsAdapter.motionValue = motionValue

            transitionScope.launch {
                motionValue.keepRunningWhile { !transition.isProgressStable || !isStable }
            }
        }

        return motionValue.output
    }

    companion object {
        /**
         * Computes the InputDirection for a triggered transition of an element appearing /
         * disappearing.
         *
         * Since [CustomPropertyTransformation] are only supported for non-shared elements, the
         * [TransitionScopedMechanicsAdapter] is only used in the context of an element appearing /
         * disappearing. This helper computes the direction to result in [InputDirection.Max] for an
         * appear transition, and [InputDirection.Min] for a disappear transition.
         */
        @VisibleForTesting
        internal fun ElementStateScope.appearDirection(
            content: ContentKey,
            element: ElementKey,
            transition: TransitionState.Transition,
        ): InputDirection {
            check(!transition.isInitiatedByUserInput)

            val inMaxDirection =
                when (transition) {
                    is TransitionState.Transition.ChangeScene -> {
                        val transitionTowardsContent = content == transition.toContent
                        val elementInContent = element.targetSize(content) != null
                        val isReversed = transition.currentScene != transition.toScene
                        (transitionTowardsContent xor elementInContent) xor !isReversed
                    }

                    is TransitionState.Transition.ShowOrHideOverlay -> {
                        val transitioningTowardsOverlay = transition.overlay == transition.toContent
                        val isReversed =
                            transitioningTowardsOverlay xor transition.isEffectivelyShown
                        transitioningTowardsOverlay xor isReversed
                    }

                    is TransitionState.Transition.ReplaceOverlay -> {
                        transition.effectivelyShownOverlay == content
                    }
                }

            return if (inMaxDirection) InputDirection.Max else InputDirection.Min
        }
    }
}
+70 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    16,
    32,
    48,
    64,
    80,
    96,
    112,
    128,
    144,
    160,
    176,
    192,
    208,
    224,
    240,
    256,
    272,
    288,
    304,
    320,
    336,
    352,
    368,
    384,
    "after"
  ],
  "features": [
    {
      "name": "Foo_yOffset",
      "type": "float",
      "data_points": [
        {
          "type": "not_found"
        },
        {
          "type": "not_found"
        },
        175,
        175,
        174.00105,
        149.84001,
        114.73702,
        0,
        0,
        0,
        0,
        10.212692,
        42.525528,
        77.174965,
        106.322296,
        128.37651,
        144.09671,
        154.88022,
        162.08202,
        166.79778,
        169.83923,
        171.77742,
        173.00056,
        173.76627,
        174.24236,
        {
          "type": "not_found"
        }
      ]
    }
  ]
}
 No newline at end of file
+48 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    16,
    32,
    48,
    64,
    80,
    96,
    112,
    128,
    144,
    160,
    176,
    192,
    208,
    224,
    240,
    256,
    "after"
  ],
  "features": [
    {
      "name": "Foo_yOffset",
      "type": "float",
      "data_points": [
        175,
        175,
        175,
        175,
        156.26086,
        121.784874,
        88.35684,
        61.32686,
        41.302353,
        27.215454,
        17.638702,
        11.284393,
        7.144104,
        4.4841614,
        2.7943878,
        1.7307587,
        1.0663452,
        0
      ]
    }
  ]
}
 No newline at end of file
+26 −0
Original line number Diff line number Diff line
{
  "frame_ids": [
    0,
    16,
    32,
    48,
    64,
    80,
    "after"
  ],
  "features": [
    {
      "name": "Foo_yOffset",
      "type": "float",
      "data_points": [
        175,
        145.83333,
        116.666664,
        87.5,
        58.33333,
        29.166672,
        0
      ]
    }
  ]
}
 No newline at end of file
Loading