• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compose.animation.scene.mechanics
18 
19 import androidx.annotation.VisibleForTesting
20 import androidx.compose.runtime.mutableFloatStateOf
21 import com.android.compose.animation.scene.ContentKey
22 import com.android.compose.animation.scene.ElementKey
23 import com.android.compose.animation.scene.ElementStateScope
24 import com.android.compose.animation.scene.content.state.TransitionState
25 import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
26 import com.android.compose.animation.scene.transformation.PropertyTransformationScope
27 import com.android.mechanics.MotionValue
28 import com.android.mechanics.ProvidedGestureContext
29 import com.android.mechanics.spec.InputDirection
30 import com.android.mechanics.spec.MotionSpec
31 import kotlinx.coroutines.CoroutineScope
32 import kotlinx.coroutines.launch
33 
34 /**
35  * Callback to create a [MotionSpec] on the first call to [CustomPropertyTransformation.transform]
36  */
37 typealias SpecFactory =
38     PropertyTransformationScope.(content: ContentKey, element: ElementKey) -> MotionSpec
39 
40 /** Callback to compute the [MotionValue] per frame */
41 typealias MotionValueInput =
42     PropertyTransformationScope.(progress: Float, content: ContentKey, element: ElementKey) -> Float
43 
44 /**
45  * Adapter to create a [MotionValue] and `keepRunning()` it temporarily while a
46  * [CustomPropertyTransformation] is in progress and until the animation settles.
47  *
48  * The [MotionValue]'s input is by default the transition progress.
49  */
50 internal class TransitionScopedMechanicsAdapter(
51     private val computeInput: MotionValueInput = { progress, _, _ -> progress },
52     private val stableThreshold: Float = MotionValue.StableThresholdEffect,
53     private val label: String? = null,
54     private val createSpec: SpecFactory,
55 ) {
56 
57     private val input = mutableFloatStateOf(0f)
58     private var motionValue: MotionValue? = null
59 
PropertyTransformationScopenull60     fun PropertyTransformationScope.update(
61         content: ContentKey,
62         element: ElementKey,
63         transition: TransitionState.Transition,
64         transitionScope: CoroutineScope,
65     ): Float {
66         val progress = transition.progressTo(content)
67         input.floatValue = computeInput(progress, content, element)
68         var motionValue = motionValue
69 
70         if (motionValue == null) {
71             motionValue =
72                 MotionValue(
73                     input::floatValue,
74                     transition.gestureContext
75                         ?: ProvidedGestureContext(
76                             0f,
77                             appearDirection(content, element, transition),
78                         ),
79                     createSpec(content, element),
80                     stableThreshold = stableThreshold,
81                     label = label,
82                 )
83             this@TransitionScopedMechanicsAdapter.motionValue = motionValue
84 
85             transitionScope.launch {
86                 motionValue.keepRunningWhile { !transition.isProgressStable || !isStable }
87             }
88         }
89 
90         return motionValue.output
91     }
92 
93     companion object {
94         /**
95          * Computes the InputDirection for a triggered transition of an element appearing /
96          * disappearing.
97          *
98          * Since [CustomPropertyTransformation] are only supported for non-shared elements, the
99          * [TransitionScopedMechanicsAdapter] is only used in the context of an element appearing /
100          * disappearing. This helper computes the direction to result in [InputDirection.Max] for an
101          * appear transition, and [InputDirection.Min] for a disappear transition.
102          */
103         @VisibleForTesting
appearDirectionnull104         internal fun ElementStateScope.appearDirection(
105             content: ContentKey,
106             element: ElementKey,
107             transition: TransitionState.Transition,
108         ): InputDirection {
109             check(!transition.isInitiatedByUserInput)
110 
111             val inMaxDirection =
112                 when (transition) {
113                     is TransitionState.Transition.ChangeScene -> {
114                         val transitionTowardsContent = content == transition.toContent
115                         val elementInContent = element.targetSize(content) != null
116                         val isReversed = transition.currentScene != transition.toScene
117                         (transitionTowardsContent xor elementInContent) xor !isReversed
118                     }
119 
120                     is TransitionState.Transition.ShowOrHideOverlay -> {
121                         val transitioningTowardsOverlay = transition.overlay == transition.toContent
122                         val isReversed =
123                             transitioningTowardsOverlay xor transition.isEffectivelyShown
124                         transitioningTowardsOverlay xor isReversed
125                     }
126 
127                     is TransitionState.Transition.ReplaceOverlay -> {
128                         transition.effectivelyShownOverlay == content
129                     }
130                 }
131 
132             return if (inMaxDirection) InputDirection.Max else InputDirection.Min
133         }
134     }
135 }
136