Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3600fb77 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Adding support for building a "spring like animation" with precomputed values

Bug: 138396187
Change-Id: Idba323090ecd9aca43c01414a32ab3b2e292e73e
parent 505a8814
Loading
Loading
Loading
Loading
+9 −12
Original line number Diff line number Diff line
@@ -18,16 +18,11 @@ package com.android.launcher3;

import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;

import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS;
import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -35,18 +30,17 @@ import android.animation.ObjectAnimator;
import android.content.Context;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.SpringObjectAnimator;
import com.android.launcher3.anim.SpringAnimationBuilder;
import com.android.quickstep.util.ClipAnimationHelper;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * A {@link QuickstepAppTransitionManagerImpl} that also implements recents transitions from
 * {@link RecentsView}.
@@ -156,8 +150,11 @@ public final class LauncherAppTransitionManagerImpl extends QuickstepAppTransiti
                return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
                        RecentsView.CONTENT_ALPHA, values);
            case INDEX_RECENTS_TRANSLATE_X_ANIM:
                return new SpringObjectAnimator<>(mLauncher.getOverviewPanel(),
                        VIEW_TRANSLATE_X, MIN_VISIBLE_CHANGE_PIXELS, 0.8f, 250, values);
                return new SpringAnimationBuilder<>(mLauncher.getOverviewPanel(), VIEW_TRANSLATE_X)
                        .setDampingRatio(0.8f)
                        .setStiffness(250)
                        .setValues(values)
                        .build(mLauncher);
            default:
                return super.createStateElementAnimation(index, values);
        }
+229 −0
Original line number Diff line number Diff line
/*
 * 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 android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.FloatProperty;

import com.android.launcher3.util.DefaultDisplay;

import androidx.annotation.FloatRange;
import androidx.dynamicanimation.animation.SpringForce;

/**
 * Utility class to build an object animator which follows the same path as a spring animation for
 * an underdamped spring.
 */
public class SpringAnimationBuilder<T> extends FloatProperty<T> {

    private final T mTarget;
    private final FloatProperty<T> mProperty;

    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 mCurrentTime = 0;

    public SpringAnimationBuilder(T target, FloatProperty<T> property) {
        super("dynamic-spring-property");
        mTarget = target;
        mProperty = property;

        mStartValue = mProperty.get(target);
    }

    public SpringAnimationBuilder<T> setEndValue(float value) {
        mEndValue = value;
        return this;
    }

    public SpringAnimationBuilder<T> setStartValue(float value) {
        mStartValue = value;
        return this;
    }

    public SpringAnimationBuilder<T> setValues(float... values) {
        if (values.length > 1) {
            mStartValue = values[0];
            mEndValue = values[values.length - 1];
        } else {
            mEndValue = values[0];
        }
        return this;
    }

    public SpringAnimationBuilder<T> 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<T> 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<T> 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<T> setStartVelocity(float startVelocity) {
        mVelocity = startVelocity;
        return this;
    }

    @Override
    public void setValue(T object, float time) {
        mCurrentTime = time;
        mProperty.setValue(
                object, (float) (exponentialComponent(time) * cosSinX(time)) + mEndValue);
    }

    @Override
    public Float get(T t) {
        return mCurrentTime;
    }

    public ObjectAnimator build(Context context) {
        int singleFrameMs = DefaultDisplay.getSingleFrameMs(context);
        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);


        long durationMs = (long) (1000.0 * duration);
        ObjectAnimator animator = ObjectAnimator.ofFloat(mTarget, this, 0, (float) duration);
        animator.setDuration(durationMs).setInterpolator(Interpolators.LINEAR);
        animator.addListener(new AnimationSuccessListener() {
            @Override
            public void onAnimationSuccess(Animator animator) {
                mProperty.setValue(mTarget, 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);
    }
}