/* * Copyright (C) 2017 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.Utilities.boundToRange; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.clampToProgress; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; import static com.android.launcher3.util.DisplayController.getSingleFrameMs; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.Context; import com.android.launcher3.Utilities; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; /** * Helper class to control the playback of an {@link AnimatorSet}, with custom interpolators * and durations. * * Note: The implementation does not support start delays on child animations or * sequential playbacks. */ public class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener { /** * Creates an animation controller for the provided animation. * The actual duration does not matter as the animation is manually controlled. It just * needs to be larger than the total number of pixels so that we don't have jittering due * to float (animation-fraction * total duration) to int conversion. */ public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) { ArrayList childAnims = new ArrayList<>(); addAnimationHoldersRecur(anim, duration, SpringProperty.DEFAULT, childAnims); return new AnimatorPlaybackController(anim, duration, childAnims); } // Progress factor after which an animation is considered almost completed. private static final float ANIMATION_COMPLETE_THRESHOLD = 0.95f; private final ValueAnimator mAnimationPlayer; private final long mDuration; private final AnimatorSet mAnim; private final Holder[] mChildAnimations; protected float mCurrentFraction; private Runnable mEndAction; protected boolean mTargetCancelled = false; /** package private */ AnimatorPlaybackController(AnimatorSet anim, long duration, ArrayList childAnims) { mAnim = anim; mDuration = duration; mAnimationPlayer = ValueAnimator.ofFloat(0, 1); mAnimationPlayer.setInterpolator(LINEAR); mAnimationPlayer.addListener(new OnAnimationEndDispatcher()); mAnimationPlayer.addUpdateListener(this); mAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationCancel(Animator animation) { mTargetCancelled = true; } @Override public void onAnimationEnd(Animator animation) { mTargetCancelled = false; } @Override public void onAnimationStart(Animator animation) { mTargetCancelled = false; } }); mChildAnimations = childAnims.toArray(new Holder[childAnims.size()]); } public AnimatorSet getTarget() { return mAnim; } public long getDuration() { return mDuration; } public TimeInterpolator getInterpolator() { return mAnim.getInterpolator() != null ? mAnim.getInterpolator() : LINEAR; } /** * Starts playing the animation forward from current position. */ public void start() { mAnimationPlayer.setFloatValues(mCurrentFraction, 1); mAnimationPlayer.setDuration(clampDuration(1 - mCurrentFraction)); mAnimationPlayer.start(); } /** * Starts playing the animation backwards from current position */ public void reverse() { mAnimationPlayer.setFloatValues(mCurrentFraction, 0); mAnimationPlayer.setDuration(clampDuration(mCurrentFraction)); mAnimationPlayer.start(); } /** * Starts playing the animation with the provided velocity optionally playing any * physics based animations. * @param goingToEnd Whether we are going to the end (progress = 1) or not (progress = 0). * @param velocityPxPerMs The velocity at which to start the animation, in pixels / millisecond. * @param endDistance The distance (pixels) that the animation will travel from progress 0 to 1. * @param animationDuration The duration of the non-physics based animation. */ public void startWithVelocity(Context context, boolean goingToEnd, float velocityPxPerMs, float endDistance, long animationDuration) { float distanceInverse = 1 / Math.abs(endDistance); float velocityProgressPerMs = velocityPxPerMs * distanceInverse; float oneFrameProgress = velocityProgressPerMs * getSingleFrameMs(context); float nextFrameProgress = boundToRange(getProgressFraction() + oneFrameProgress, 0f, 1f); // Update setters for spring int springFlag = goingToEnd ? SpringProperty.FLAG_CAN_SPRING_ON_END : SpringProperty.FLAG_CAN_SPRING_ON_START; long springDuration = animationDuration; for (Holder h : mChildAnimations) { if ((h.springProperty.flags & springFlag) != 0) { SpringAnimationBuilder s = new SpringAnimationBuilder(context) .setStartValue(mCurrentFraction) .setEndValue(goingToEnd ? 1 : 0) .setStartVelocity(velocityProgressPerMs) .setMinimumVisibleChange(distanceInverse) .setDampingRatio(h.springProperty.mDampingRatio) .setStiffness(h.springProperty.mStiffness) .computeParams(); long expectedDurationL = s.getDuration(); springDuration = Math.max(expectedDurationL, springDuration); float expectedDuration = expectedDurationL; h.mapper = (progress, globalEndProgress) -> { if (expectedDuration <= 0 || oneFrameProgress >= 1) { return 1; } else { // Start from one frame ahead of the current position. return Utilities.mapToRange( mAnimationPlayer.getCurrentPlayTime() / expectedDuration, 0, 1, Math.abs(oneFrameProgress), 1, LINEAR); } }; h.anim.setInterpolator(s::getInterpolatedValue); } } mAnimationPlayer.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f); if (springDuration <= animationDuration) { mAnimationPlayer.setDuration(animationDuration); mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocityPxPerMs)); } else { // Since spring requires more time to run, we let the other animations play with // current time and interpolation and by clamping the duration. mAnimationPlayer.setDuration(springDuration); float cutOff = animationDuration / (float) springDuration; mAnimationPlayer.setInterpolator( clampToProgress(scrollInterpolatorForVelocity(velocityPxPerMs), 0, cutOff)); } mAnimationPlayer.start(); } /** * Tries to finish the running animation if it is close to completion. */ public void forceFinishIfCloseToEnd() { if (mAnimationPlayer.isRunning() && mAnimationPlayer.getAnimatedFraction() > ANIMATION_COMPLETE_THRESHOLD) { mAnimationPlayer.end(); } } /** * Pauses the currently playing animation. */ public void pause() { // Reset property setters for (Holder h : mChildAnimations) { h.reset(); } mAnimationPlayer.cancel(); } /** * Returns the underlying animation used for controlling the set. */ public ValueAnimator getAnimationPlayer() { return mAnimationPlayer; } /** * Sets the current animation position and updates all the child animators accordingly. */ public void setPlayFraction(float fraction) { mCurrentFraction = fraction; // Let the animator report the progress but don't apply the progress to child // animations if it has been cancelled. if (mTargetCancelled) { return; } float progress = boundToRange(fraction, 0, 1); for (Holder holder : mChildAnimations) { holder.setProgress(progress); } } public float getProgressFraction() { return mCurrentFraction; } public float getInterpolatedProgress() { return getInterpolator().getInterpolation(mCurrentFraction); } /** * Sets the action to be called when the animation is completed. Also clears any * previously set action. */ public void setEndAction(Runnable runnable) { mEndAction = runnable; } @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { setPlayFraction((float) valueAnimator.getAnimatedValue()); } protected long clampDuration(float fraction) { float playPos = mDuration * fraction; if (playPos <= 0) { return 0; } else { return Math.min((long) playPos, mDuration); } } public AnimatorPlaybackController dispatchOnStart() { callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart); return this; } public AnimatorPlaybackController dispatchOnCancel() { callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationCancel); return this; } public AnimatorPlaybackController dispatchOnEnd() { callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd); return this; } public void dispatchSetInterpolator(TimeInterpolator interpolator) { callAnimatorCommandRecursively(mAnim, a -> a.setInterpolator(interpolator)); } /** * Recursively calls a command on all the listeners of the provided animation */ public static void callListenerCommandRecursively( Animator anim, BiConsumer command) { callAnimatorCommandRecursively(anim, a-> { for (AnimatorListener l : nonNullList(a.getListeners())) { command.accept(l, a); } }); } private static void callAnimatorCommandRecursively(Animator anim, Consumer command) { command.accept(anim); if (anim instanceof AnimatorSet) { for (Animator child : nonNullList(((AnimatorSet) anim).getChildAnimations())) { callAnimatorCommandRecursively(child, command); } } } /** * Only dispatches the on end actions once the animator and all springs have completed running. */ private class OnAnimationEndDispatcher extends AnimationSuccessListener { boolean mDispatched = false; @Override public void onAnimationStart(Animator animation) { mCancelled = false; mDispatched = false; } @Override public void onAnimationSuccess(Animator animator) { // We wait for the spring (if any) to finish running before completing the end callback. if (!mDispatched) { dispatchOnEnd(); if (mEndAction != null) { mEndAction.run(); } mDispatched = true; } } } private static List nonNullList(ArrayList list) { return list == null ? Collections.emptyList() : list; } /** * Interface for mapping progress to animation progress */ private interface ProgressMapper { ProgressMapper DEFAULT = (progress, globalEndProgress) -> progress > globalEndProgress ? 1 : (progress / globalEndProgress); float getProgress(float progress, float globalProgress); } /** * Holder class for various child animations */ static class Holder { public final ValueAnimator anim; public final SpringProperty springProperty; public final TimeInterpolator interpolator; public final float globalEndProgress; public ProgressMapper mapper; Holder(Animator anim, float globalDuration, SpringProperty springProperty) { this.anim = (ValueAnimator) anim; this.springProperty = springProperty; this.interpolator = this.anim.getInterpolator(); this.globalEndProgress = anim.getDuration() / globalDuration; this.mapper = ProgressMapper.DEFAULT; } public void setProgress(float progress) { anim.setCurrentFraction(mapper.getProgress(progress, globalEndProgress)); } public void reset() { anim.setInterpolator(interpolator); mapper = ProgressMapper.DEFAULT; } } static void addAnimationHoldersRecur(Animator anim, long globalDuration, SpringProperty springProperty, ArrayList out) { long forceDuration = anim.getDuration(); TimeInterpolator forceInterpolator = anim.getInterpolator(); if (anim instanceof ValueAnimator) { out.add(new Holder(anim, globalDuration, springProperty)); } else if (anim instanceof AnimatorSet) { for (Animator child : ((AnimatorSet) anim).getChildAnimations()) { if (forceDuration > 0) { child.setDuration(forceDuration); } if (forceInterpolator != null) { child.setInterpolator(forceInterpolator); } addAnimationHoldersRecur(child, globalDuration, springProperty, out); } } else { throw new RuntimeException("Unknown animation type " + anim); } } }