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