• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.mechanics
18 
19 import androidx.compose.runtime.FloatState
20 import androidx.compose.runtime.derivedStateOf
21 import androidx.compose.runtime.getValue
22 import androidx.compose.runtime.mutableFloatStateOf
23 import androidx.compose.runtime.mutableLongStateOf
24 import androidx.compose.runtime.mutableStateOf
25 import androidx.compose.runtime.referentialEqualityPolicy
26 import androidx.compose.runtime.setValue
27 import androidx.compose.runtime.snapshotFlow
28 import androidx.compose.runtime.withFrameNanos
29 import com.android.mechanics.debug.DebugInspector
30 import com.android.mechanics.debug.FrameData
31 import com.android.mechanics.impl.Computations
32 import com.android.mechanics.impl.DiscontinuityAnimation
33 import com.android.mechanics.impl.GuaranteeState
34 import com.android.mechanics.spec.Breakpoint
35 import com.android.mechanics.spec.Guarantee
36 import com.android.mechanics.spec.InputDirection
37 import com.android.mechanics.spec.Mapping
38 import com.android.mechanics.spec.MotionSpec
39 import com.android.mechanics.spec.SegmentData
40 import com.android.mechanics.spring.SpringState
41 import java.util.concurrent.atomic.AtomicInteger
42 import kotlinx.coroutines.CoroutineName
43 import kotlinx.coroutines.flow.first
44 import kotlinx.coroutines.withContext
45 
46 /**
47  * Computes an animated [output] value, by mapping the [currentInput] according to the [spec].
48  *
49  * A [MotionValue] represents a single animated value within a larger animation. It takes a
50  * numerical [currentInput] value, typically a spatial value like width, height, or gesture length,
51  * and transforms it into an [output] value using a [MotionSpec].
52  *
53  * ## Mapping Input to Output
54  *
55  * The [MotionSpec] defines the relationship between the input and output values. It does this by
56  * specifying a series of [Mapping] functions and [Breakpoint]s. Breakpoints divide the input domain
57  * into segments. Each segment has an associated [Mapping] function, which determines how input
58  * values within that segment are transformed into output values.
59  *
60  * These [Mapping] functions can be arbitrary, as long as they are
61  * 1. deterministic: When invoked repeatedly for the same input, they must produce the same output.
62  * 2. continuous: meaning infinitesimally small changes in input result in infinitesimally small
63  *    changes in output
64  *
65  * A valid [Mapping] function is one whose graph could be drawn without lifting your pen from the
66  * paper, meaning there are no abrupt jumps or breaks.
67  *
68  * ## Animating Discontinuities
69  *
70  * When the input value crosses a breakpoint, there might be a discontinuity in the output value due
71  * to the switch between mapping functions. `MotionValue` automatically animates these
72  * discontinuities using a spring animation. The spring parameters are defined for each
73  * [Breakpoint].
74  *
75  * ## Guarantees for Choreography
76  *
77  * Breakpoints can also define [Guarantee]s. These guarantees can make the spring animation finish
78  * faster, in response to quick input value changes. Thus, [Guarantee]s allows to maintain a
79  * predictable choreography, even as the input is unpredictably changed by a user's gesture.
80  *
81  * ## Updating the MotionSpec
82  *
83  * The [spec] property can be changed at any time. If the new spec produces a different output for
84  * the current input, the difference will be animated using the spring parameters defined in
85  * [MotionSpec.resetSpring].
86  *
87  * ## Gesture Context
88  *
89  * The [GestureContext] augments the [currentInput] value with the user's intent. The
90  * [GestureContext] is created wherever gesture input is handled. If the motion value is not driven
91  * by a gesture, it is OK for the [GestureContext] to return static values.
92  *
93  * ## Usage
94  *
95  * The [MotionValue] does animate the [output] implicitly, whenever a change in [currentInput],
96  * [spec], or [gestureContext] requires it. The animated value is computed whenever the [output]
97  * property is read, or the latest once the animation frame is complete.
98  * 1. Create an instance, providing the input value, gesture context, and an initial spec.
99  * 2. Call [keepRunning] in a coroutine scope, and keep the coroutine running while the
100  *    `MotionValue` is in use.
101  * 3. Access the animated output value through the [output] property.
102  *
103  * Internally, the [keepRunning] coroutine is automatically suspended if there is nothing to
104  * animate.
105  *
106  * @param currentInput Provides the current input value.
107  * @param gestureContext The [GestureContext] augmenting the [currentInput].
108  * @param label An optional label to aid in debugging.
109  * @param stableThreshold A threshold value (in output units) that determines when the
110  *   [MotionValue]'s internal spring animation is considered stable.
111  */
112 class MotionValue(
113     currentInput: () -> Float,
114     gestureContext: GestureContext,
115     initialSpec: MotionSpec = MotionSpec.Empty,
116     label: String? = null,
117     stableThreshold: Float = StableThresholdEffect,
118 ) : FloatState {
119     private val impl =
120         ObservableComputations(currentInput, gestureContext, initialSpec, stableThreshold, label)
121 
122     /** The [MotionSpec] describing the mapping of this [MotionValue]'s input to the output. */
123     var spec: MotionSpec by impl::spec
124 
125     /** Animated [output] value. */
126     val output: Float by impl::output
127 
128     /**
129      * [output] value, but without animations.
130      *
131      * This value always reports the target value, even before a animation is finished.
132      *
133      * While [isStable], [outputTarget] and [output] are the same value.
134      */
135     val outputTarget: Float by impl::outputTarget
136 
137     /** The [output] exposed as [FloatState]. */
138     override val floatValue: Float by impl::output
139 
140     /** Whether an animation is currently running. */
141     val isStable: Boolean by impl::isStable
142 
143     /**
144      * Keeps the [MotionValue]'s animated output running.
145      *
146      * Clients must call [keepRunning], and keep the coroutine running while the [MotionValue] is in
147      * use. When disposing this [MotionValue], cancel the coroutine.
148      *
149      * Internally, this method does suspend, unless there are animations ongoing.
150      */
151     suspend fun keepRunning(): Nothing {
152         withContext(CoroutineName("MotionValue($label)")) { impl.keepRunning { true } }
153 
154         // `keepRunning` above will never finish,
155         throw AssertionError("Unreachable code")
156     }
157 
158     /**
159      * Keeps the [MotionValue]'s animated output running while [continueRunning] returns `true`.
160      *
161      * When [continueRunning] returns `false`, the coroutine will end by the next frame.
162      *
163      * To keep the [MotionValue] running until the current animations are complete, check for
164      * `isStable` as well.
165      *
166      * ```kotlin
167      * motionValue.keepRunningWhile { !shouldEnd() || !isStable }
168      * ```
169      */
170     suspend fun keepRunningWhile(continueRunning: MotionValue.() -> Boolean) =
171         withContext(CoroutineName("MotionValue($label)")) {
172             impl.keepRunning { continueRunning.invoke(this@MotionValue) }
173         }
174 
175     val label: String? by impl::label
176 
177     companion object {
178         /** Creates a [MotionValue] whose [currentInput] is the animated [output] of [source]. */
179         fun createDerived(
180             source: MotionValue,
181             initialSpec: MotionSpec = MotionSpec.Empty,
182             label: String? = null,
183             stableThreshold: Float = 0.01f,
184         ): MotionValue {
185             return MotionValue(
186                 currentInput = source::output,
187                 gestureContext = source.impl.gestureContext,
188                 initialSpec = initialSpec,
189                 label = label,
190                 stableThreshold = stableThreshold,
191             )
192         }
193 
194         const val StableThresholdEffect = 0.01f
195         const val StableThresholdSpatial = 1f
196 
197         internal const val TAG = "MotionValue"
198     }
199 
200     private var debugInspectorRefCount = AtomicInteger(0)
201 
202     private fun onDisposeDebugInspector() {
203         if (debugInspectorRefCount.decrementAndGet() == 0) {
204             impl.debugInspector = null
205         }
206     }
207 
208     /**
209      * Provides access to internal state for debug tooling and tests.
210      *
211      * The returned [DebugInspector] must be [DebugInspector.dispose]d when no longer needed.
212      */
213     fun debugInspector(): DebugInspector {
214         if (debugInspectorRefCount.getAndIncrement() == 0) {
215             impl.debugInspector =
216                 DebugInspector(
217                     FrameData(
218                         impl.lastInput,
219                         impl.lastSegment.direction,
220                         impl.lastGestureDragOffset,
221                         impl.lastFrameTimeNanos,
222                         impl.lastSpringState,
223                         impl.lastSegment,
224                         impl.lastAnimation,
225                     ),
226                     impl.isActive,
227                     impl.debugIsAnimating,
228                     ::onDisposeDebugInspector,
229                 )
230         }
231 
232         return checkNotNull(impl.debugInspector)
233     }
234 }
235 
236 private class ObservableComputations(
237     val input: () -> Float,
238     val gestureContext: GestureContext,
239     initialSpec: MotionSpec = MotionSpec.Empty,
240     override val stableThreshold: Float,
241     override val label: String?,
242 ) : Computations {
243 
244     // ----  CurrentFrameInput ---------------------------------------------------------------------
245 
246     override var spec by mutableStateOf(initialSpec)
247     override val currentInput: Float
248         get() = input.invoke()
249 
250     override val currentDirection: InputDirection
251         get() = gestureContext.direction
252 
253     override val currentGestureDragOffset: Float
254         get() = gestureContext.dragOffset
255 
256     override var currentAnimationTimeNanos by mutableLongStateOf(-1L)
257 
258     // ----  LastFrameState ---------------------------------------------------------------------
259 
260     override var lastSegment: SegmentData by
261         mutableStateOf(
262             spec.segmentAtInput(currentInput, currentDirection),
263             referentialEqualityPolicy(),
264         )
265 
266     override var lastGuaranteeState: GuaranteeState
267         get() = GuaranteeState(_lastGuaranteeStatePacked)
268         set(value) {
269             _lastGuaranteeStatePacked = value.packedValue
270         }
271 
272     private var _lastGuaranteeStatePacked: Long by
273         mutableLongStateOf(GuaranteeState.Inactive.packedValue)
274 
275     override var lastAnimation: DiscontinuityAnimation by
276         mutableStateOf(DiscontinuityAnimation.None, referentialEqualityPolicy())
277 
278     override var directMappedVelocity: Float = 0f
279 
280     override var lastSpringState: SpringState
281         get() = SpringState(_lastSpringStatePacked)
282         set(value) {
283             _lastSpringStatePacked = value.packedValue
284         }
285 
286     private var _lastSpringStatePacked: Long by
287         mutableLongStateOf(lastAnimation.springStartState.packedValue)
288 
289     override var lastFrameTimeNanos by mutableLongStateOf(-1L)
290 
291     override var lastInput by mutableFloatStateOf(currentInput)
292 
293     override var lastGestureDragOffset by mutableFloatStateOf(currentGestureDragOffset)
294 
295     // ---- Computations ---------------------------------------------------------------------------
296 
<lambda>null297     override val currentSegment by derivedStateOf { computeCurrentSegment() }
<lambda>null298     override val currentGuaranteeState by derivedStateOf { computeCurrentGuaranteeState() }
<lambda>null299     override val currentAnimation by derivedStateOf { computeCurrentAnimation() }
<lambda>null300     override val currentSpringState by derivedStateOf { computeCurrentSpringState() }
301 
keepRunningnull302     suspend fun keepRunning(continueRunning: () -> Boolean) {
303         check(!isActive) { "MotionValue($label) is already running" }
304         isActive = true
305 
306         // These `captured*` values will be applied to the `last*` values, at the beginning
307         // of the each new frame.
308         // TODO(b/397837971): Encapsulate the state in a StateRecord.
309         var capturedSegment = currentSegment
310         var capturedGuaranteeState = currentGuaranteeState
311         var capturedAnimation = currentAnimation
312         var capturedSpringState = currentSpringState
313         var capturedFrameTimeNanos = currentAnimationTimeNanos
314         var capturedInput = currentInput
315         var capturedGestureDragOffset = currentGestureDragOffset
316         var capturedDirection = currentDirection
317 
318         try {
319             debugIsAnimating = true
320 
321             // indicates whether withFrameNanos is called continuously (as opposed to being
322             // suspended for an undetermined amount of time in between withFrameNanos).
323             // This is essential after `withFrameNanos` returned: if true at this point,
324             // currentAnimationTimeNanos - lastFrameNanos is the duration of the last frame.
325             var isAnimatingUninterrupted = false
326 
327             while (continueRunning()) {
328 
329                 withFrameNanos { frameTimeNanos ->
330                     currentAnimationTimeNanos = frameTimeNanos
331 
332                     // With the new frame started, copy
333 
334                     lastSegment = capturedSegment
335                     lastGuaranteeState = capturedGuaranteeState
336                     lastAnimation = capturedAnimation
337                     lastSpringState = capturedSpringState
338                     lastFrameTimeNanos = capturedFrameTimeNanos
339                     lastInput = capturedInput
340                     lastGestureDragOffset = capturedGestureDragOffset
341                 }
342 
343                 // At this point, the complete frame is done (including layout, drawing and
344                 // everything else), and this MotionValue has been updated.
345 
346                 // Capture the `current*` MotionValue state, so that it can be applied as the
347                 // `last*` state when the next frame starts. Its imperative to capture at this point
348                 // already (since the input could change before the next frame starts), while at the
349                 // same time not already applying the `last*` state (as this would cause a
350                 // re-computation if the current state is being read before the next frame).
351                 if (isAnimatingUninterrupted) {
352                     val currentDirectMapped = currentDirectMapped
353                     val lastDirectMapped =
354                         lastSegment.mapping.map(lastInput) - lastAnimation.targetValue
355 
356                     val frameDuration =
357                         (currentAnimationTimeNanos - lastFrameTimeNanos) / 1_000_000_000.0
358                     val staticDelta = (currentDirectMapped - lastDirectMapped)
359                     directMappedVelocity = (staticDelta / frameDuration).toFloat()
360                 } else {
361                     directMappedVelocity = 0f
362                 }
363 
364                 var scheduleNextFrame = !isStable
365                 if (capturedSegment != currentSegment) {
366                     capturedSegment = currentSegment
367                     scheduleNextFrame = true
368                 }
369 
370                 if (capturedGuaranteeState != currentGuaranteeState) {
371                     capturedGuaranteeState = currentGuaranteeState
372                     scheduleNextFrame = true
373                 }
374 
375                 if (capturedAnimation != currentAnimation) {
376                     capturedAnimation = currentAnimation
377                     scheduleNextFrame = true
378                 }
379 
380                 if (capturedSpringState != currentSpringState) {
381                     capturedSpringState = currentSpringState
382                     scheduleNextFrame = true
383                 }
384 
385                 if (capturedInput != currentInput) {
386                     capturedInput = currentInput
387                     scheduleNextFrame = true
388                 }
389 
390                 if (capturedGestureDragOffset != currentGestureDragOffset) {
391                     capturedGestureDragOffset = currentGestureDragOffset
392                     scheduleNextFrame = true
393                 }
394 
395                 if (capturedDirection != currentDirection) {
396                     capturedDirection = currentDirection
397                     scheduleNextFrame = true
398                 }
399 
400                 capturedFrameTimeNanos = currentAnimationTimeNanos
401 
402                 debugInspector?.run {
403                     frame =
404                         FrameData(
405                             capturedInput,
406                             capturedDirection,
407                             capturedGestureDragOffset,
408                             capturedFrameTimeNanos,
409                             capturedSpringState,
410                             capturedSegment,
411                             capturedAnimation,
412                         )
413                 }
414 
415                 isAnimatingUninterrupted = scheduleNextFrame
416                 if (scheduleNextFrame) {
417                     continue
418                 }
419 
420                 debugIsAnimating = false
421                 snapshotFlow {
422                         val wakeup =
423                             !continueRunning() ||
424                                 spec != capturedSegment.spec ||
425                                 currentInput != capturedInput ||
426                                 currentDirection != capturedDirection ||
427                                 currentGestureDragOffset != capturedGestureDragOffset
428                         wakeup
429                     }
430                     .first { it }
431                 debugIsAnimating = true
432             }
433         } finally {
434             isActive = false
435             debugIsAnimating = false
436         }
437     }
438 
439     /** Whether a [keepRunning] coroutine is active currently. */
440     var isActive = false
441         set(value) {
442             field = value
443             debugInspector?.isActive = value
444         }
445 
446     /**
447      * `false` whenever the [keepRunning] coroutine is suspended while no animation is running and
448      * the input is not changing.
449      */
450     var debugIsAnimating = false
451         set(value) {
452             field = value
453             debugInspector?.isAnimating = value
454         }
455 
456     var debugInspector: DebugInspector? = null
457 }
458