• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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.mechanics.impl
18 
19 import android.util.Log
20 import androidx.compose.ui.util.fastCoerceAtLeast
21 import androidx.compose.ui.util.fastCoerceIn
22 import androidx.compose.ui.util.fastIsFinite
23 import androidx.compose.ui.util.lerp
24 import com.android.mechanics.MotionValue.Companion.TAG
25 import com.android.mechanics.spec.Guarantee
26 import com.android.mechanics.spec.SegmentData
27 import com.android.mechanics.spring.SpringState
28 import com.android.mechanics.spring.calculateUpdatedState
29 
30 internal interface ComputeSegment : CurrentFrameInput, LastFrameState, StaticConfig {
31     /**
32      * The current segment, which defines the [Mapping] function used to transform the input to the
33      * output.
34      *
35      * While both [spec] and [currentDirection] remain the same, and [currentInput] is within the
36      * segment (see [SegmentData.isValidForInput]), this is [lastSegment].
37      *
38      * Otherwise, [MotionSpec.onChangeSegment] is queried for an up-dated segment.
39      */
computeCurrentSegmentnull40     fun computeCurrentSegment(): SegmentData {
41         val lastSegment = lastSegment
42         val input = currentInput
43         val direction = currentDirection
44 
45         val specChanged = lastSegment.spec != spec
46         return if (specChanged || !lastSegment.isValidForInput(input, direction)) {
47             spec.onChangeSegment(lastSegment, input, direction)
48         } else {
49             lastSegment
50         }
51     }
52 }
53 
54 internal interface ComputeGuaranteeState : ComputeSegment {
55     val currentSegment: SegmentData
56 
57     /** Computes the [SegmentChangeType] between [lastSegment] and [currentSegment]. */
58     val segmentChangeType: SegmentChangeType
59         get() {
60             val currentSegment = currentSegment
61             val lastSegment = lastSegment
62 
63             if (currentSegment.key == lastSegment.key) {
64                 return SegmentChangeType.Same
65             }
66 
67             if (
68                 currentSegment.key.minBreakpoint == lastSegment.key.minBreakpoint &&
69                     currentSegment.key.maxBreakpoint == lastSegment.key.maxBreakpoint
70             ) {
71                 return SegmentChangeType.SameOppositeDirection
72             }
73 
74             val currentSpec = currentSegment.spec
75             val lastSpec = lastSegment.spec
76             if (currentSpec !== lastSpec) {
77                 // Determine/guess whether the segment change was due to the changed spec, or
78                 // whether lastSpec would return the same segment key for the update input.
79                 val lastSpecSegmentForSameInput =
80                     lastSpec.segmentAtInput(currentInput, currentDirection).key
81                 if (currentSegment.key != lastSpecSegmentForSameInput) {
82                     // Note: this might not be correct if the new [MotionSpec.segmentHandlers] were
83                     // involved.
84                     return SegmentChangeType.Spec
85                 }
86             }
87 
88             return if (currentSegment.direction == lastSegment.direction) {
89                 SegmentChangeType.Traverse
90             } else {
91                 SegmentChangeType.Direction
92             }
93         }
94 
95     /**
96      * Computes the fraction of [position] between [lastInput] and [currentInput].
97      *
98      * Essentially, this determines fractionally when [position] was crossed, between the current
99      * frame and the last frame.
100      *
101      * Since frames are updated periodically, not continuously, crossing a breakpoint happened
102      * sometime between the last frame's start and this frame's start.
103      *
104      * This fraction is used to estimate the time when a breakpoint was crossed since last frame,
105      * and simplifies the logic of crossing multiple breakpoints in one frame, as it offers the
106      * springs and guarantees time to be updated correctly.
107      *
108      * Of course, this is a simplification that assumes the input velocity was uniform during the
109      * last frame, but that is likely good enough.
110      */
lastFrameFractionOfPositionnull111     fun lastFrameFractionOfPosition(position: Float): Float {
112         return ((position - lastInput) / (currentInput - lastInput)).fastCoerceIn(0f, 1f)
113     }
114 
115     /**
116      * The [GuaranteeState] for [currentSegment].
117      *
118      * Without a segment change, this carries forward [lastGuaranteeState], adjusted to the new
119      * input if needed.
120      *
121      * If a segment change happened, this is a new [GuaranteeState] for the [currentSegment]. Any
122      * remaining [lastGuaranteeState] will be consumed in [currentAnimation].
123      */
computeCurrentGuaranteeStatenull124     fun computeCurrentGuaranteeState(): GuaranteeState {
125         val currentSegment = currentSegment
126         val entryBreakpoint = currentSegment.entryBreakpoint
127 
128         // First, determine the origin of the guarantee computations
129         val guaranteeOriginState =
130             when (segmentChangeType) {
131                 // Still in the segment, the origin is carried over from the last frame
132                 SegmentChangeType.Same -> lastGuaranteeState
133                 // The direction changed within the same segment, no guarantee to enforce.
134                 SegmentChangeType.SameOppositeDirection -> return GuaranteeState.Inactive
135                 // The spec changes, there is no guarantee associated with the animation.
136                 SegmentChangeType.Spec -> return GuaranteeState.Inactive
137                 SegmentChangeType.Direction -> {
138                     // Direction changed over a segment boundary. To make up for the
139                     // directionChangeSlop, the guarantee starts at the current input.
140                     GuaranteeState.withStartValue(
141                         when (entryBreakpoint.guarantee) {
142                             is Guarantee.InputDelta -> currentInput
143                             is Guarantee.GestureDragDelta -> currentGestureDragOffset
144                             is Guarantee.None -> return GuaranteeState.Inactive
145                         }
146                     )
147                 }
148 
149                 SegmentChangeType.Traverse -> {
150                     // Traversed over a segment boundary, the guarantee going forward is determined
151                     // by the [entryBreakpoint].
152                     GuaranteeState.withStartValue(
153                         when (entryBreakpoint.guarantee) {
154                             is Guarantee.InputDelta -> entryBreakpoint.position
155                             is Guarantee.GestureDragDelta -> {
156                                 // Guess the GestureDragDelta origin - since the gesture dragOffset
157                                 // is sampled, interpolate it according to when the breakpoint was
158                                 // crossed in the last frame.
159                                 val fractionalBreakpointPos =
160                                     lastFrameFractionOfPosition(entryBreakpoint.position)
161 
162                                 lerp(
163                                     lastGestureDragOffset,
164                                     currentGestureDragOffset,
165                                     fractionalBreakpointPos,
166                                 )
167                             }
168 
169                             // No guarantee to enforce.
170                             is Guarantee.None -> return GuaranteeState.Inactive
171                         }
172                     )
173                 }
174             }
175 
176         // Finally, update the origin state with the current guarantee value.
177         return guaranteeOriginState.withCurrentValue(
178             when (entryBreakpoint.guarantee) {
179                 is Guarantee.InputDelta -> currentInput
180                 is Guarantee.GestureDragDelta -> currentGestureDragOffset
181                 is Guarantee.None -> return GuaranteeState.Inactive
182             },
183             currentSegment.direction,
184         )
185     }
186 }
187 
188 internal interface ComputeAnimation : ComputeGuaranteeState {
189     val currentGuaranteeState: GuaranteeState
190 
191     /**
192      * The [DiscontinuityAnimation] in effect for the current frame.
193      *
194      * This describes the starting condition of the spring animation, and is only updated if the
195      * spring animation must restarted: that is, if yet another discontinuity must be animated as a
196      * result of a segment change, or if the [currentGuaranteeState] requires the spring to be
197      * tightened.
198      *
199      * See [currentSpringState] for the continuously updated, animated spring values.
200      */
computeCurrentAnimationnull201     fun computeCurrentAnimation(): DiscontinuityAnimation {
202         val currentSegment = currentSegment
203         val lastSegment = lastSegment
204         val currentSpec = spec
205         val currentInput = currentInput
206         val lastAnimation = lastAnimation
207 
208         return when (segmentChangeType) {
209             SegmentChangeType.Same -> {
210                 if (lastAnimation.isAtRest) {
211                     // Nothing to update if no animation is ongoing
212                     lastAnimation
213                 } else if (lastGuaranteeState == currentGuaranteeState) {
214                     // Nothing to update if the spring must not be tightened.
215                     lastAnimation
216                 } else {
217                     // Compute the updated spring parameters
218                     val tightenedSpringParameters =
219                         currentGuaranteeState.updatedSpringParameters(
220                             currentSegment.entryBreakpoint
221                         )
222 
223                     lastAnimation.copy(
224                         springStartState = lastSpringState,
225                         springParameters = tightenedSpringParameters,
226                         springStartTimeNanos = lastFrameTimeNanos,
227                     )
228                 }
229             }
230 
231             SegmentChangeType.SameOppositeDirection,
232             SegmentChangeType.Direction,
233             SegmentChangeType.Spec -> {
234                 // Determine the delta in the output, as produced by the old and new mapping.
235                 val currentMapping = currentSegment.mapping.map(currentInput)
236                 val lastMapping = lastSegment.mapping.map(currentInput)
237                 val delta = currentMapping - lastMapping
238 
239                 val deltaIsFinite = delta.fastIsFinite()
240                 if (!deltaIsFinite) {
241                     Log.wtf(
242                         TAG,
243                         "Delta between mappings is undefined!\n" +
244                             "  MotionValue: $label\n" +
245                             "  input: $currentInput\n" +
246                             "  lastMapping: $lastMapping (lastSegment: $lastSegment)\n" +
247                             "  currentMapping: $currentMapping (currentSegment: $currentSegment)",
248                     )
249                 }
250 
251                 if (delta == 0f || !deltaIsFinite) {
252                     // Nothing new to animate.
253                     lastAnimation
254                 } else {
255                     val springParameters =
256                         if (segmentChangeType == SegmentChangeType.Direction) {
257                             currentSegment.entryBreakpoint.spring
258                         } else {
259                             currentSpec.resetSpring
260                         }
261 
262                     val newTarget = delta - lastSpringState.displacement
263                     DiscontinuityAnimation(
264                         newTarget,
265                         SpringState(-newTarget, lastSpringState.velocity + directMappedVelocity),
266                         springParameters,
267                         lastFrameTimeNanos,
268                     )
269                 }
270             }
271 
272             SegmentChangeType.Traverse -> {
273                 // Process all breakpoints traversed, in order.
274                 // This is involved due to the guarantees - they have to be applied, one after the
275                 // other, before crossing the next breakpoint.
276                 val currentDirection = currentSegment.direction
277 
278                 with(currentSpec[currentDirection]) {
279                     val targetIndex = findSegmentIndex(currentSegment.key)
280                     val sourceIndex = findSegmentIndex(lastSegment.key)
281                     check(targetIndex != sourceIndex)
282 
283                     val directionOffset = if (targetIndex > sourceIndex) 1 else -1
284 
285                     var lastBreakpoint = lastSegment.entryBreakpoint
286                     var lastAnimationTime = lastFrameTimeNanos
287                     var guaranteeState = lastGuaranteeState
288                     var springState = lastSpringState
289                     var springTarget = lastAnimation.targetValue
290                     var springParameters = lastAnimation.springParameters
291 
292                     var segmentIndex = sourceIndex
293                     while (segmentIndex != targetIndex) {
294                         val nextBreakpoint =
295                             breakpoints[segmentIndex + directionOffset.fastCoerceAtLeast(0)]
296 
297                         val nextBreakpointFrameFraction =
298                             lastFrameFractionOfPosition(nextBreakpoint.position)
299 
300                         val nextBreakpointCrossTime =
301                             lerp(
302                                 lastFrameTimeNanos,
303                                 currentAnimationTimeNanos,
304                                 nextBreakpointFrameFraction,
305                             )
306                         if (
307                             guaranteeState != GuaranteeState.Inactive &&
308                                 springState != SpringState.AtRest
309                         ) {
310                             val guaranteeValueAtNextBreakpoint =
311                                 when (lastBreakpoint.guarantee) {
312                                     is Guarantee.InputDelta -> nextBreakpoint.position
313                                     is Guarantee.GestureDragDelta ->
314                                         lerp(
315                                             lastGestureDragOffset,
316                                             currentGestureDragOffset,
317                                             nextBreakpointFrameFraction,
318                                         )
319 
320                                     is Guarantee.None ->
321                                         error(
322                                             "guaranteeState ($guaranteeState) is not Inactive, guarantee is missing"
323                                         )
324                                 }
325 
326                             guaranteeState =
327                                 guaranteeState.withCurrentValue(
328                                     guaranteeValueAtNextBreakpoint,
329                                     currentDirection,
330                                 )
331 
332                             springParameters =
333                                 guaranteeState.updatedSpringParameters(lastBreakpoint)
334                         }
335 
336                         springState =
337                             springState.calculateUpdatedState(
338                                 nextBreakpointCrossTime - lastAnimationTime,
339                                 springParameters,
340                             )
341                         lastAnimationTime = nextBreakpointCrossTime
342 
343                         val mappingBefore = mappings[segmentIndex]
344                         val beforeBreakpoint = mappingBefore.map(nextBreakpoint.position)
345                         val mappingAfter = mappings[segmentIndex + directionOffset]
346                         val afterBreakpoint = mappingAfter.map(nextBreakpoint.position)
347 
348                         val delta = afterBreakpoint - beforeBreakpoint
349                         val deltaIsFinite = delta.fastIsFinite()
350                         if (!deltaIsFinite) {
351                             Log.wtf(
352                                 TAG,
353                                 "Delta between breakpoints is undefined!\n" +
354                                     "  MotionValue: $label\n" +
355                                     "  position: ${nextBreakpoint.position}\n" +
356                                     "  before: $beforeBreakpoint (mapping: $mappingBefore)\n" +
357                                     "  after: $afterBreakpoint (mapping: $mappingAfter)",
358                             )
359                         }
360 
361                         if (deltaIsFinite) {
362                             springTarget += delta
363                             springState = springState.nudge(displacementDelta = -delta)
364                         }
365                         segmentIndex += directionOffset
366                         lastBreakpoint = nextBreakpoint
367                         guaranteeState =
368                             when (nextBreakpoint.guarantee) {
369                                 is Guarantee.InputDelta ->
370                                     GuaranteeState.withStartValue(nextBreakpoint.position)
371 
372                                 is Guarantee.GestureDragDelta ->
373                                     GuaranteeState.withStartValue(
374                                         lerp(
375                                             lastGestureDragOffset,
376                                             currentGestureDragOffset,
377                                             nextBreakpointFrameFraction,
378                                         )
379                                     )
380 
381                                 is Guarantee.None -> GuaranteeState.Inactive
382                             }
383                     }
384 
385                     if (springState.displacement != 0f) {
386                         springState = springState.nudge(velocityDelta = directMappedVelocity)
387                     }
388 
389                     val tightened =
390                         currentGuaranteeState.updatedSpringParameters(
391                             currentSegment.entryBreakpoint
392                         )
393 
394                     DiscontinuityAnimation(springTarget, springState, tightened, lastAnimationTime)
395                 }
396             }
397         }
398     }
399 }
400 
401 internal interface ComputeSpringState : ComputeAnimation {
402     val currentAnimation: DiscontinuityAnimation
403 
computeCurrentSpringStatenull404     fun computeCurrentSpringState(): SpringState {
405         with(currentAnimation) {
406             if (isAtRest) return SpringState.AtRest
407 
408             val nanosSinceAnimationStart = currentAnimationTimeNanos - springStartTimeNanos
409             val updatedSpringState =
410                 springStartState.calculateUpdatedState(nanosSinceAnimationStart, springParameters)
411 
412             return if (updatedSpringState.isStable(springParameters, stableThreshold)) {
413                 SpringState.AtRest
414             } else {
415                 updatedSpringState
416             }
417         }
418     }
419 }
420 
421 internal interface Computations : ComputeSpringState {
422     val currentSpringState: SpringState
423 
424     val currentDirectMapped: Float
425         get() = currentSegment.mapping.map(currentInput) - currentAnimation.targetValue
426 
427     val currentAnimatedDelta: Float
428         get() = currentAnimation.targetValue + currentSpringState.displacement
429 
430     val output: Float
431         get() = currentDirectMapped + currentAnimatedDelta
432 
433     val outputTarget: Float
434         get() = currentDirectMapped + currentAnimation.targetValue
435 
436     val isStable: Boolean
437         get() = currentSpringState == SpringState.AtRest
438 }
439