f0c7966a77
Animations should only be canceled (as opposed to ended) if the caller is intending to update the state from there. In that case, we shouldn't jump to the end value. Bug: 155335494 Change-Id: I93a21f0e2e1923ccdfab890adcb58c49ceb98a28
234 lines
7.7 KiB
Java
234 lines
7.7 KiB
Java
/*
|
|
* 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.DefaultDisplay;
|
|
|
|
/**
|
|
* 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 = DefaultDisplay.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);
|
|
}
|
|
}
|