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