/* * Copyright (C) 2014 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.systemui.statusbar; import android.animation.Animator; import android.content.Context; import android.view.ViewPropertyAnimator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import com.android.systemui.Interpolators; /** * Utility class to calculate general fling animation when the finger is released. */ public class FlingAnimationUtils { private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f; private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f; private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f; private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f; private static final float MIN_VELOCITY_DP_PER_SECOND = 250; private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000; /** * Crazy math. http://en.wikipedia.org/wiki/B%C3%A9zier_curve */ private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 1.0f / LINEAR_OUT_SLOW_IN_X2; private Interpolator mLinearOutSlowIn; private float mMinVelocityPxPerSecond; private float mMaxLengthSeconds; private float mHighVelocityPxPerSecond; private AnimatorProperties mAnimatorProperties = new AnimatorProperties(); public FlingAnimationUtils(Context ctx, float maxLengthSeconds) { mMaxLengthSeconds = maxLengthSeconds; mLinearOutSlowIn = new PathInterpolator(0, 0, LINEAR_OUT_SLOW_IN_X2, 1); mMinVelocityPxPerSecond = MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density; mHighVelocityPxPerSecond = HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density; } /** * Applies the interpolator and length to the animator, such that the fling animation is * consistent with the finger motion. * * @param animator the animator to apply * @param currValue the current value * @param endValue the end value of the animator * @param velocity the current velocity of the motion */ public void apply(Animator animator, float currValue, float endValue, float velocity) { apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); } /** * Applies the interpolator and length to the animator, such that the fling animation is * consistent with the finger motion. * * @param animator the animator to apply * @param currValue the current value * @param endValue the end value of the animator * @param velocity the current velocity of the motion */ public void apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity) { apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); } /** * Applies the interpolator and length to the animator, such that the fling animation is * consistent with the finger motion. * * @param animator the animator to apply * @param currValue the current value * @param endValue the end value of the animator * @param velocity the current velocity of the motion * @param maxDistance the maximum distance for this interaction; the maximum animation length * gets multiplied by the ratio between the actual distance and this value */ public void apply(Animator animator, float currValue, float endValue, float velocity, float maxDistance) { AnimatorProperties properties = getProperties(currValue, endValue, velocity, maxDistance); animator.setDuration(properties.duration); animator.setInterpolator(properties.interpolator); } /** * Applies the interpolator and length to the animator, such that the fling animation is * consistent with the finger motion. * * @param animator the animator to apply * @param currValue the current value * @param endValue the end value of the animator * @param velocity the current velocity of the motion * @param maxDistance the maximum distance for this interaction; the maximum animation length * gets multiplied by the ratio between the actual distance and this value */ public void apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance) { AnimatorProperties properties = getProperties(currValue, endValue, velocity, maxDistance); animator.setDuration(properties.duration); animator.setInterpolator(properties.interpolator); } private AnimatorProperties getProperties(float currValue, float endValue, float velocity, float maxDistance) { float maxLengthSeconds = (float) (mMaxLengthSeconds * Math.sqrt(Math.abs(endValue - currValue) / maxDistance)); float diff = Math.abs(endValue - currValue); float velAbs = Math.abs(velocity); float durationSeconds = LINEAR_OUT_SLOW_IN_START_GRADIENT * diff / velAbs; if (durationSeconds <= maxLengthSeconds) { mAnimatorProperties.interpolator = mLinearOutSlowIn; } else if (velAbs >= mMinVelocityPxPerSecond) { // Cross fade between fast-out-slow-in and linear interpolator with current velocity. durationSeconds = maxLengthSeconds; VelocityInterpolator velocityInterpolator = new VelocityInterpolator(durationSeconds, velAbs, diff); InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( velocityInterpolator, mLinearOutSlowIn, mLinearOutSlowIn); mAnimatorProperties.interpolator = superInterpolator; } else { // Just use a normal interpolator which doesn't take the velocity into account. durationSeconds = maxLengthSeconds; mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN; } mAnimatorProperties.duration = (long) (durationSeconds * 1000); return mAnimatorProperties; } /** * Applies the interpolator and length to the animator, such that the fling animation is * consistent with the finger motion for the case when the animation is making something * disappear. * * @param animator the animator to apply * @param currValue the current value * @param endValue the end value of the animator * @param velocity the current velocity of the motion * @param maxDistance the maximum distance for this interaction; the maximum animation length * gets multiplied by the ratio between the actual distance and this value */ public void applyDismissing(Animator animator, float currValue, float endValue, float velocity, float maxDistance) { AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, maxDistance); animator.setDuration(properties.duration); animator.setInterpolator(properties.interpolator); } /** * Applies the interpolator and length to the animator, such that the fling animation is * consistent with the finger motion for the case when the animation is making something * disappear. * * @param animator the animator to apply * @param currValue the current value * @param endValue the end value of the animator * @param velocity the current velocity of the motion * @param maxDistance the maximum distance for this interaction; the maximum animation length * gets multiplied by the ratio between the actual distance and this value */ public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance) { AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, maxDistance); animator.setDuration(properties.duration); animator.setInterpolator(properties.interpolator); } private AnimatorProperties getDismissingProperties(float currValue, float endValue, float velocity, float maxDistance) { float maxLengthSeconds = (float) (mMaxLengthSeconds * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f)); float diff = Math.abs(endValue - currValue); float velAbs = Math.abs(velocity); float y2 = calculateLinearOutFasterInY2(velAbs); float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2; Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2); float durationSeconds = startGradient * diff / velAbs; if (durationSeconds <= maxLengthSeconds) { mAnimatorProperties.interpolator = mLinearOutFasterIn; } else if (velAbs >= mMinVelocityPxPerSecond) { // Cross fade between linear-out-faster-in and linear interpolator with current // velocity. durationSeconds = maxLengthSeconds; VelocityInterpolator velocityInterpolator = new VelocityInterpolator(durationSeconds, velAbs, diff); InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( velocityInterpolator, mLinearOutFasterIn, mLinearOutSlowIn); mAnimatorProperties.interpolator = superInterpolator; } else { // Just use a normal interpolator which doesn't take the velocity into account. durationSeconds = maxLengthSeconds; mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN; } mAnimatorProperties.duration = (long) (durationSeconds * 1000); return mAnimatorProperties; } /** * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the * velocity. The faster the velocity, the more "linear" the interpolator gets. * * @param velocity the velocity of the gesture. * @return the y2 control point for a cubic bezier path interpolator */ private float calculateLinearOutFasterInY2(float velocity) { float t = (velocity - mMinVelocityPxPerSecond) / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond); t = Math.max(0, Math.min(1, t)); return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX; } /** * @return the minimum velocity a gesture needs to have to be considered a fling */ public float getMinVelocityPxPerSecond() { return mMinVelocityPxPerSecond; } /** * An interpolator which interpolates two interpolators with an interpolator. */ private static final class InterpolatorInterpolator implements Interpolator { private Interpolator mInterpolator1; private Interpolator mInterpolator2; private Interpolator mCrossfader; InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader) { mInterpolator1 = interpolator1; mInterpolator2 = interpolator2; mCrossfader = crossfader; } @Override public float getInterpolation(float input) { float t = mCrossfader.getInterpolation(input); return (1 - t) * mInterpolator1.getInterpolation(input) + t * mInterpolator2.getInterpolation(input); } } /** * An interpolator which interpolates with a fixed velocity. */ private static final class VelocityInterpolator implements Interpolator { private float mDurationSeconds; private float mVelocity; private float mDiff; private VelocityInterpolator(float durationSeconds, float velocity, float diff) { mDurationSeconds = durationSeconds; mVelocity = velocity; mDiff = diff; } @Override public float getInterpolation(float input) { float time = input * mDurationSeconds; return time * mVelocity / mDiff; } } private static class AnimatorProperties { Interpolator interpolator; long duration; } }