commonMain.androidx.compose.foundation.gestures.UpdatableAnimationState.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of foundation-desktop Show documentation
Show all versions of foundation-desktop Show documentation
Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers
The newest version!
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation.gestures
import androidx.compose.animation.core.AnimationConstants.UnspecifiedTime
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.internal.checkPrecondition
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.MotionDurationScale
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.coroutines.coroutineContext
import kotlin.math.absoluteValue
import kotlin.math.roundToLong
/**
* Holds state for an [animation][animateToZero] that will continuously animate a float [value] to
* zero.
*
* Unlike the standard [AnimationState], this class allows the value to be changed while the
* animation is running. When that happens, the next frame will continue animating the new value to
* zero as though the previous animation was interrupted and restarted with the new value. See the
* docs on [animateToZero] for more information.
*
* An analogy for how this animation works is gravity – you can pick something up, and as soon as
* you let it go it will start falling to the ground. If you catch it and raise it higher, it will
* continue falling from the new height.
*
* Similar behavior could be achieved by using an [AnimationState] and creating a new copy and
* launching a new coroutine to call `animateTo(0f)` every time the value changes. However, this
* class doesn't require allocating a new state object and launching/cancelling a coroutine to
* update the value, which makes for a more convenient API for this particular use case, and makes
* it cheaper to update [value] on every frame.
*/
internal class UpdatableAnimationState(animationSpec: AnimationSpec) {
private val vectorizedSpec = animationSpec.vectorize(Float.VectorConverter)
private var lastFrameTime = UnspecifiedTime
private var lastVelocity = ZeroVector
private var isRunning = false
/**
* The value to be animated. This property will be changed on every frame while [animateToZero]
* is running, and will be set to exactly 0f before it returns. Unlike [AnimationState], this
* property is mutable – it can be changed it any time during the animation, and the animation
* will continue running from the new value on the next frame.
*
* Simply setting this property will not start the animation – [animateToZero] must be manually
* invoked to kick off the animation, but once it's running it does not need to be called again
* when this property is changed, until the animation finishes.
*/
var value: Float = 0f
/**
* Starts animating [value] to 0f. This function will suspend until [value] actually reaches 0f
* – e.g. if [value] is reset to a non-zero value on every frame, it will never return. When
* this function does return, [value] will have been set to exactly 0f.
*
* If this function is called more than once concurrently, it will throw.
*
* @param beforeFrame Called _inside_ the choreographer callback on every frame with the
* difference between the previous value and the new value. This corresponds to the typical
* frame callback used in the other animation APIs and [withFrameNanos]. It runs before
* composition, layout, and other passes for the frame.
* @param afterFrame Called _outside_ the choreographer callback for every frame, _after_ the
* composition and layout passes have finished running for that frame. This function allows
* the caller to update [value] based on any layout changes performed in [beforeFrame].
*/
@OptIn(ExperimentalContracts::class)
suspend fun animateToZero(
beforeFrame: (valueDelta: Float) -> Unit,
afterFrame: () -> Unit,
) {
contract { callsInPlace(beforeFrame) }
checkPrecondition(!isRunning) { "animateToZero called while previous animation is running" }
val durationScale = coroutineContext[MotionDurationScale]?.scaleFactor ?: 1f
isRunning = true
try {
// Don't rely on the animation's duration vs playtime to calculate completion since the
// value could be updated after each frame, and if that happens we need to continue
// running the animation.
while (!value.isZeroish()) {
withFrameNanos { frameTime ->
if (lastFrameTime == UnspecifiedTime) {
lastFrameTime = frameTime
}
val vectorizedCurrentValue = AnimationVector1D(value)
val playTime =
if (durationScale == 0f) {
// The duration scale will be 0 when animations are disabled via a11y
// settings or developer settings.
vectorizedSpec.getDurationNanos(
initialValue = AnimationVector1D(value),
targetValue = ZeroVector,
initialVelocity = lastVelocity
)
} else {
((frameTime - lastFrameTime) / durationScale).roundToLong()
}
val newValue =
vectorizedSpec
.getValueFromNanos(
playTimeNanos = playTime,
initialValue = vectorizedCurrentValue,
targetValue = ZeroVector,
initialVelocity = lastVelocity
)
.value
lastVelocity =
vectorizedSpec.getVelocityFromNanos(
playTimeNanos = playTime,
initialValue = vectorizedCurrentValue,
targetValue = ZeroVector,
initialVelocity = lastVelocity
)
lastFrameTime = frameTime
val delta = value - newValue
value = newValue
beforeFrame(delta)
}
afterFrame()
if (durationScale == 0f) {
// Never run more than one loop when animations are disabled.
break
}
}
// The last iteration of the loop may have called block with a non-zero value due to
// the visibility threshold, so ensure it gets called one last time with actual zero.
if (value.absoluteValue != 0f) {
withFrameNanos {
val delta = value
// Update the value before invoking the callback so that the callback will see
// the correct value if it looks at it.
value = 0f
beforeFrame(delta)
}
afterFrame()
}
} finally {
lastFrameTime = UnspecifiedTime
lastVelocity = ZeroVector
isRunning = false
}
}
private companion object {
const val VisibilityThreshold = 0.01f
val ZeroVector = AnimationVector1D(0f)
fun Float.isZeroish() = absoluteValue < VisibilityThreshold
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy