mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-03 17:36:49 +00:00
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.window.RefreshRateTracker;
|
|
|
|
/**
|
|
* 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 = RefreshRateTracker.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);
|
|
}
|
|
}
|