/* * 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 ValueAnimator build(T target, FloatProperty 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); } }