blob: bd52158f0a7432134e6944e3e462c2e6176a3ae5 [file] [log] [blame]
/*
* Copyright (C) 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 com.android.launcher3.anim;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.FloatProperty;
import androidx.annotation.FloatRange;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.launcher3.util.DisplayController;
/**
* Utility class to build an object animator which follows the same path as a spring animation for
* an underdamped spring.
*/
public class SpringAnimationBuilder {
private final Context mContext;
private float mStartValue;
private float mEndValue;
private float mVelocity = 0;
private float mStiffness = SpringForce.STIFFNESS_MEDIUM;
private float mDampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
private float mMinVisibleChange = 1;
// Multiplier to the min visible change value for value threshold
private static final float THRESHOLD_MULTIPLIER = 0.65f;
/**
* The spring equation is given as
* x = e^(-beta*t/2) * (a cos(gamma * t) + b sin(gamma * t)
* v = e^(-beta*t/2) * ((2 * a * gamma + beta * b) * sin(gamma * t)
* + (a * beta - 2 * b * gamma) * cos(gamma * t)) / 2
*
* a = x(0)
* b = beta * x(0) / (2 * gamma) + v(0) / gamma
*/
private double beta;
private double gamma;
private double a, b;
private double va, vb;
// Threshold for velocity and value to determine when it's reasonable to assume that the spring
// is approximately at rest.
private double mValueThreshold;
private double mVelocityThreshold;
private float mDuration = 0;
public SpringAnimationBuilder(Context context) {
mContext = context;
}
public SpringAnimationBuilder setEndValue(float value) {
mEndValue = value;
return this;
}
public SpringAnimationBuilder setStartValue(float value) {
mStartValue = value;
return this;
}
public SpringAnimationBuilder setValues(float... values) {
if (values.length > 1) {
mStartValue = values[0];
mEndValue = values[values.length - 1];
} else {
mEndValue = values[0];
}
return this;
}
public SpringAnimationBuilder setStiffness(
@FloatRange(from = 0.0, fromInclusive = false) float stiffness) {
if (stiffness <= 0) {
throw new IllegalArgumentException("Spring stiffness constant must be positive.");
}
mStiffness = stiffness;
return this;
}
public SpringAnimationBuilder setDampingRatio(
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
float dampingRatio) {
if (dampingRatio <= 0 || dampingRatio >= 1) {
throw new IllegalArgumentException("Damping ratio must be between 0 and 1");
}
mDampingRatio = dampingRatio;
return this;
}
public SpringAnimationBuilder setMinimumVisibleChange(
@FloatRange(from = 0.0, fromInclusive = false) float minimumVisibleChange) {
if (minimumVisibleChange <= 0) {
throw new IllegalArgumentException("Minimum visible change must be positive.");
}
mMinVisibleChange = minimumVisibleChange;
return this;
}
public SpringAnimationBuilder setStartVelocity(float startVelocity) {
mVelocity = startVelocity;
return this;
}
public float getInterpolatedValue(float fraction) {
return getValue(mDuration * fraction);
}
private float getValue(float time) {
return (float) (exponentialComponent(time) * cosSinX(time)) + mEndValue;
}
public SpringAnimationBuilder computeParams() {
int singleFrameMs = DisplayController.getSingleFrameMs(mContext);
double naturalFreq = Math.sqrt(mStiffness);
double dampedFreq = naturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
// All the calculations assume the stable position to be 0, shift the values accordingly.
beta = 2 * mDampingRatio * naturalFreq;
gamma = dampedFreq;
a = mStartValue - mEndValue;
b = beta * a / (2 * gamma) + mVelocity / gamma;
va = a * beta / 2 - b * gamma;
vb = a * gamma + beta * b / 2;
mValueThreshold = mMinVisibleChange * THRESHOLD_MULTIPLIER;
// 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.
mVelocityThreshold = mValueThreshold * 1000.0 / singleFrameMs;
// Find the duration (in seconds) for the spring to reach equilibrium.
// equilibrium is reached when x = 0
double duration = Math.atan2(-a, b) / gamma;
// Keep moving ahead until the velocity reaches equilibrium.
double piByG = Math.PI / gamma;
while (duration < 0 || Math.abs(exponentialComponent(duration) * cosSinV(duration))
>= mVelocityThreshold) {
duration += piByG;
}
// Find the shortest time
double edgeTime = Math.max(0, duration - piByG / 2);
double minDiff = singleFrameMs / 2000.0; // Half frame time in seconds
do {
if ((duration - edgeTime) < minDiff) {
break;
}
double mid = (edgeTime + duration) / 2;
if (isAtEquilibrium(mid)) {
duration = mid;
} else {
edgeTime = mid;
}
} while (true);
mDuration = (float) duration;
return this;
}
public long getDuration() {
return (long) (1000.0 * mDuration);
}
public <T> ValueAnimator build(T target, FloatProperty<T> property) {
computeParams();
ValueAnimator animator = ValueAnimator.ofFloat(0, mDuration);
animator.setDuration(getDuration()).setInterpolator(LINEAR);
animator.addUpdateListener(anim ->
property.set(target, getInterpolatedValue(anim.getAnimatedFraction())));
animator.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animation) {
property.set(target, mEndValue);
}
});
return animator;
}
private boolean isAtEquilibrium(double t) {
double ec = exponentialComponent(t);
if (Math.abs(ec * cosSinX(t)) >= mValueThreshold) {
return false;
}
return Math.abs(ec * cosSinV(t)) < mVelocityThreshold;
}
private double exponentialComponent(double t) {
return Math.pow(Math.E, - beta * t / 2);
}
private double cosSinX(double t) {
return cosSin(t, a, b);
}
private double cosSinV(double t) {
return cosSin(t, va, vb);
}
private double cosSin(double t, double cosFactor, double sinFactor) {
double angle = t * gamma;
return cosFactor * Math.cos(angle) + sinFactor * Math.sin(angle);
}
}