1 /*
<lambda>null2  * Copyright 2022 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 androidx.compose.foundation.gestures
18 
19 import androidx.compose.animation.core.AnimationConstants.UnspecifiedTime
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.AnimationState
22 import androidx.compose.animation.core.AnimationVector1D
23 import androidx.compose.animation.core.VectorConverter
24 import androidx.compose.foundation.internal.checkPrecondition
25 import androidx.compose.runtime.withFrameNanos
26 import androidx.compose.ui.MotionDurationScale
27 import kotlin.contracts.ExperimentalContracts
28 import kotlin.contracts.contract
29 import kotlin.coroutines.coroutineContext
30 import kotlin.math.absoluteValue
31 import kotlin.math.roundToLong
32 
33 /**
34  * Holds state for an [animation][animateToZero] that will continuously animate a float [value] to
35  * zero.
36  *
37  * Unlike the standard [AnimationState], this class allows the value to be changed while the
38  * animation is running. When that happens, the next frame will continue animating the new value to
39  * zero as though the previous animation was interrupted and restarted with the new value. See the
40  * docs on [animateToZero] for more information.
41  *
42  * An analogy for how this animation works is gravity – you can pick something up, and as soon as
43  * you let it go it will start falling to the ground. If you catch it and raise it higher, it will
44  * continue falling from the new height.
45  *
46  * Similar behavior could be achieved by using an [AnimationState] and creating a new copy and
47  * launching a new coroutine to call `animateTo(0f)` every time the value changes. However, this
48  * class doesn't require allocating a new state object and launching/cancelling a coroutine to
49  * update the value, which makes for a more convenient API for this particular use case, and makes
50  * it cheaper to update [value] on every frame.
51  */
52 internal class UpdatableAnimationState(animationSpec: AnimationSpec<Float>) {
53 
54     private val vectorizedSpec = animationSpec.vectorize(Float.VectorConverter)
55     private var lastFrameTime = UnspecifiedTime
56     private var lastVelocity = ZeroVector
57     private var isRunning = false
58 
59     /**
60      * The value to be animated. This property will be changed on every frame while [animateToZero]
61      * is running, and will be set to exactly 0f before it returns. Unlike [AnimationState], this
62      * property is mutable – it can be changed it any time during the animation, and the animation
63      * will continue running from the new value on the next frame.
64      *
65      * Simply setting this property will not start the animation – [animateToZero] must be manually
66      * invoked to kick off the animation, but once it's running it does not need to be called again
67      * when this property is changed, until the animation finishes.
68      */
69     var value: Float = 0f
70 
71     /**
72      * Starts animating [value] to 0f. This function will suspend until [value] actually reaches 0f
73      * – e.g. if [value] is reset to a non-zero value on every frame, it will never return. When
74      * this function does return, [value] will have been set to exactly 0f.
75      *
76      * If this function is called more than once concurrently, it will throw.
77      *
78      * @param beforeFrame Called _inside_ the choreographer callback on every frame with the
79      *   difference between the previous value and the new value. This corresponds to the typical
80      *   frame callback used in the other animation APIs and [withFrameNanos]. It runs before
81      *   composition, layout, and other passes for the frame.
82      * @param afterFrame Called _outside_ the choreographer callback for every frame, _after_ the
83      *   composition and layout passes have finished running for that frame. This function allows
84      *   the caller to update [value] based on any layout changes performed in [beforeFrame].
85      */
86     @OptIn(ExperimentalContracts::class)
87     @Suppress("LEAKED_IN_PLACE_LAMBDA")
88     suspend fun animateToZero(
89         beforeFrame: (valueDelta: Float) -> Unit,
90         afterFrame: () -> Unit,
91     ) {
92         contract { callsInPlace(beforeFrame) }
93         checkPrecondition(!isRunning) { "animateToZero called while previous animation is running" }
94 
95         val durationScale = coroutineContext[MotionDurationScale]?.scaleFactor ?: 1f
96         isRunning = true
97 
98         try {
99             // Don't rely on the animation's duration vs playtime to calculate completion since the
100             // value could be updated after each frame, and if that happens we need to continue
101             // running the animation.
102             while (!value.isZeroish()) {
103                 withFrameNanos { frameTime ->
104                     if (lastFrameTime == UnspecifiedTime) {
105                         lastFrameTime = frameTime
106                     }
107 
108                     val vectorizedCurrentValue = AnimationVector1D(value)
109                     val playTime =
110                         if (durationScale == 0f) {
111                             // The duration scale will be 0 when animations are disabled via a11y
112                             // settings or developer settings.
113                             vectorizedSpec.getDurationNanos(
114                                 initialValue = AnimationVector1D(value),
115                                 targetValue = ZeroVector,
116                                 initialVelocity = lastVelocity
117                             )
118                         } else {
119                             ((frameTime - lastFrameTime) / durationScale).roundToLong()
120                         }
121                     val newValue =
122                         vectorizedSpec
123                             .getValueFromNanos(
124                                 playTimeNanos = playTime,
125                                 initialValue = vectorizedCurrentValue,
126                                 targetValue = ZeroVector,
127                                 initialVelocity = lastVelocity
128                             )
129                             .value
130                     lastVelocity =
131                         vectorizedSpec.getVelocityFromNanos(
132                             playTimeNanos = playTime,
133                             initialValue = vectorizedCurrentValue,
134                             targetValue = ZeroVector,
135                             initialVelocity = lastVelocity
136                         )
137                     lastFrameTime = frameTime
138 
139                     val delta = value - newValue
140                     value = newValue
141                     beforeFrame(delta)
142                 }
143                 afterFrame()
144 
145                 if (durationScale == 0f) {
146                     // Never run more than one loop when animations are disabled.
147                     break
148                 }
149             }
150 
151             // The last iteration of the loop may have called block with a non-zero value due to
152             // the visibility threshold, so ensure it gets called one last time with actual zero.
153             if (value.absoluteValue != 0f) {
154                 withFrameNanos {
155                     val delta = value
156                     // Update the value before invoking the callback so that the callback will see
157                     // the correct value if it looks at it.
158                     value = 0f
159                     beforeFrame(delta)
160                 }
161                 afterFrame()
162             }
163         } finally {
164             lastFrameTime = UnspecifiedTime
165             lastVelocity = ZeroVector
166             isRunning = false
167         }
168     }
169 
170     private companion object {
171         const val VisibilityThreshold = 0.01f
172         val ZeroVector = AnimationVector1D(0f)
173 
174         fun Float.isZeroish() = absoluteValue < VisibilityThreshold
175     }
176 }
177