[go: nahoru, domu]

blob: e7fd777456215492ce7893da30fbe36381d332bc [file] [log] [blame]
/*
* Copyright 2019 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.animation.core
import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.unpackFloat1
import androidx.compose.ui.util.unpackFloat2
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.sin
import kotlin.math.sqrt
/**
* Spring Simulation simulates spring physics, and allows you to query the motion (i.e. value and
* velocity) at certain time in the future based on the starting velocity and value.
*
* By configuring the stiffness and damping ratio, callers can create a spring with the look and
* feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring
* is, the harder it is to stretch it, the faster it undergoes dampening.
*
*
* Spring damping ratio describes how oscillations in a system decay after a disturbance.
* When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position
* without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
* return to equilibrium within the shortest amount of time. When damping ratio is less than 1
* (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any
* damping (i.e. damping ratio = 0), the mass will oscillate forever.
*/
@Suppress("INLINE_CLASS_DEPRECATED", "EXPERIMENTAL_FEATURE_WARNING")
internal inline class Motion(val packedValue: Long) {
val value: Float
get() = unpackFloat1(packedValue)
val velocity: Float
get() = unpackFloat2(packedValue)
/**
* Returns a copy of this Motion instance optionally overriding the
* value or velocity parameters
*/
fun copy(value: Float = this.value, velocity: Float = this.velocity) = Motion(value, velocity)
}
internal fun Motion(value: Float, velocity: Float) = Motion(packFloats(value, velocity))
// This multiplier is used to calculate the velocity threshold given a certain value threshold.
// The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity
// is a reasonable threshold.
private const val VelocityThresholdMultiplier = 1000.0 / 16.0
// Value to indicate an unset state.
internal val UNSET = Float.MAX_VALUE
internal class SpringSimulation(var finalPosition: Float) {
// Natural frequency
private var naturalFreq = sqrt(Spring.StiffnessVeryLow.toDouble())
// Indicates whether the spring has been initialized
private var initialized = false
// Intermediate values to simplify the spring function calculation per frame.
private var gammaPlus: Double = 0.0
private var gammaMinus: Double = 0.0
private var dampedFreq: Double = 0.0
/**
* Stiffness of the spring.
*/
var stiffness: Float
set(value) {
if (stiffness <= 0) {
throw IllegalArgumentException("Spring stiffness constant must be positive.")
}
naturalFreq = sqrt(value.toDouble())
// All the intermediate values need to be recalculated.
initialized = false
}
get() {
return (naturalFreq * naturalFreq).toFloat()
}
/**
* Returns the damping ratio of the spring.
*
* @return damping ratio of the spring
*/
var dampingRatio: Float = Spring.DampingRatioNoBouncy
set(value) {
if (value < 0) {
throw IllegalArgumentException("Damping ratio must be non-negative")
}
field = value
// All the intermediate values need to be recalculated.
initialized = false
}
/*********************** Below are private APIs */
fun getAcceleration(lastDisplacement: Float, lastVelocity: Float): Float {
val adjustedDisplacement = lastDisplacement - finalPosition
val k = naturalFreq * naturalFreq
val c = 2.0 * naturalFreq * dampingRatio
return (-k * adjustedDisplacement - c * lastVelocity).toFloat()
}
/**
* Initialize the string by doing the necessary pre-calculation as well as some validity check
* on the setup.
*
* @throws IllegalStateException if the final position is not yet set by the time the spring
* animation has started
*/
private fun init() {
if (initialized) {
return
}
if (finalPosition == UNSET) {
throw IllegalStateException(
"Error: Final position of the spring must be set before the animation starts"
)
}
val dampingRatioSquared = dampingRatio * dampingRatio.toDouble()
if (dampingRatio > 1) {
// Over damping
gammaPlus =
(-dampingRatio * naturalFreq + naturalFreq * sqrt(dampingRatioSquared - 1))
gammaMinus =
(-dampingRatio * naturalFreq - naturalFreq * sqrt(dampingRatioSquared - 1))
} else if (dampingRatio >= 0 && dampingRatio < 1) {
// Under damping
dampedFreq = naturalFreq * sqrt(1 - dampingRatioSquared)
}
initialized = true
}
/**
* Internal only call for Spring to calculate the spring position/velocity using
* an analytical approach.
*/
internal fun updateValues(lastDisplacement: Float, lastVelocity: Float, timeElapsed: Long):
Motion {
init()
val adjustedDisplacement = lastDisplacement - finalPosition
val deltaT = timeElapsed / 1000.0 // unit: seconds
val displacement: Double
val currentVelocity: Double
if (dampingRatio > 1) {
// Overdamped
val coeffA =
(
adjustedDisplacement - (
(gammaMinus * adjustedDisplacement - lastVelocity) /
(gammaMinus - gammaPlus)
)
)
val coeffB = (
(gammaMinus * adjustedDisplacement - lastVelocity) /
(gammaMinus - gammaPlus)
)
displacement = (
coeffA * exp(gammaMinus * deltaT) +
coeffB * exp(gammaPlus * deltaT)
)
currentVelocity = (
coeffA * gammaMinus * exp(gammaMinus * deltaT) +
coeffB * gammaPlus * exp(gammaPlus * deltaT)
)
} else if (dampingRatio == 1.0f) {
// Critically damped
val coeffA = adjustedDisplacement
val coeffB = lastVelocity + naturalFreq * adjustedDisplacement
displacement = (coeffA + coeffB * deltaT) * exp(-naturalFreq * deltaT)
currentVelocity =
(
(
(coeffA + coeffB * deltaT) * exp(-naturalFreq * deltaT) *
(-naturalFreq)
) + coeffB * exp(-naturalFreq * deltaT)
)
} else {
// Underdamped
val cosCoeff = adjustedDisplacement
val sinCoeff =
(
(1 / dampedFreq) * (
(
(dampingRatio * naturalFreq * adjustedDisplacement) +
lastVelocity
)
)
)
displacement = (
exp(-dampingRatio * naturalFreq * deltaT) *
(
(
cosCoeff * cos(dampedFreq * deltaT) +
sinCoeff * sin(dampedFreq * deltaT)
)
)
)
currentVelocity = (
displacement * (-naturalFreq) * dampingRatio + (
exp(
-dampingRatio * naturalFreq * deltaT
) * (
(
-dampedFreq * cosCoeff *
sin(dampedFreq * deltaT) + dampedFreq * sinCoeff *
cos(dampedFreq * deltaT)
)
)
)
)
}
val newValue = (displacement + finalPosition).toFloat()
val newVelocity = currentVelocity.toFloat()
return Motion(newValue, newVelocity)
}
}