| /* |
| * 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.anim.Interpolators.LINEAR; |
| import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; |
| |
| 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.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import androidx.dynamicanimation.animation.DynamicAnimation; |
| import androidx.dynamicanimation.animation.SpringAnimation; |
| |
| /** |
| * 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 abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener { |
| |
| private static final String TAG = "AnimatorPlaybackCtrler"; |
| private static boolean DEBUG = false; |
| |
| public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) { |
| return wrap(anim, duration, null); |
| } |
| |
| /** |
| * 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, |
| Runnable onCancelRunnable) { |
| |
| /** |
| * TODO: use {@link AnimatorSet#setCurrentPlayTime(long)} once b/68382377 is fixed. |
| */ |
| return new AnimatorPlaybackControllerVL(anim, duration, onCancelRunnable); |
| } |
| |
| private final ValueAnimator mAnimationPlayer; |
| private final long mDuration; |
| |
| protected final AnimatorSet mAnim; |
| private Set<SpringAnimation> mSprings; |
| |
| protected float mCurrentFraction; |
| private Runnable mEndAction; |
| |
| protected boolean mTargetCancelled = false; |
| protected Runnable mOnCancelRunnable; |
| |
| private OnAnimationEndDispatcher mEndListener; |
| private DynamicAnimation.OnAnimationEndListener mSpringEndListener; |
| // We need this variable to ensure the end listener is called immediately, otherwise we run into |
| // issues where the callback interferes with the states of the swipe detector. |
| private boolean mSkipToEnd = false; |
| |
| protected AnimatorPlaybackController(AnimatorSet anim, long duration, |
| Runnable onCancelRunnable) { |
| mAnim = anim; |
| mDuration = duration; |
| mOnCancelRunnable = onCancelRunnable; |
| |
| mAnimationPlayer = ValueAnimator.ofFloat(0, 1); |
| mAnimationPlayer.setInterpolator(LINEAR); |
| mEndListener = new OnAnimationEndDispatcher(); |
| mAnimationPlayer.addListener(mEndListener); |
| mAnimationPlayer.addUpdateListener(this); |
| |
| mAnim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| mTargetCancelled = true; |
| if (mOnCancelRunnable != null) { |
| mOnCancelRunnable.run(); |
| mOnCancelRunnable = null; |
| } |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mTargetCancelled = false; |
| mOnCancelRunnable = null; |
| } |
| |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mTargetCancelled = false; |
| } |
| }); |
| |
| mSprings = new HashSet<>(); |
| mSpringEndListener = (animation, canceled, value, velocity1) -> { |
| if (canceled) { |
| mEndListener.onAnimationCancel(mAnimationPlayer); |
| } else { |
| mEndListener.onAnimationEnd(mAnimationPlayer); |
| } |
| }; |
| } |
| |
| 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(); |
| } |
| |
| /** |
| * Pauses the currently playing animation. |
| */ |
| public void pause() { |
| 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 abstract void setPlayFraction(float fraction); |
| |
| 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); |
| } |
| } |
| |
| /** |
| * Starts playback and sets the spring. |
| */ |
| public void dispatchOnStartWithVelocity(float end, float velocity) { |
| if (!QUICKSTEP_SPRINGS.get()) { |
| dispatchOnStart(); |
| return; |
| } |
| |
| if (DEBUG) Log.d(TAG, "dispatchOnStartWithVelocity#end=" + end + ", velocity=" + velocity); |
| |
| for (Animator a : mAnim.getChildAnimations()) { |
| if (a instanceof SpringObjectAnimator) { |
| if (DEBUG) Log.d(TAG, "Found springAnimator=" + a); |
| SpringObjectAnimator springAnimator = (SpringObjectAnimator) a; |
| mSprings.add(springAnimator.getSpring()); |
| springAnimator.startSpring(end, velocity, mSpringEndListener); |
| } |
| } |
| |
| dispatchOnStart(); |
| } |
| |
| public void dispatchOnStart() { |
| dispatchOnStartRecursively(mAnim); |
| } |
| |
| private void dispatchOnStartRecursively(Animator animator) { |
| List<AnimatorListener> listeners = animator instanceof SpringObjectAnimator |
| ? nonNullList(((SpringObjectAnimator) animator).getObjectAnimatorListeners()) |
| : nonNullList(animator.getListeners()); |
| |
| for (AnimatorListener l : listeners) { |
| l.onAnimationStart(animator); |
| } |
| |
| if (animator instanceof AnimatorSet) { |
| for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) { |
| dispatchOnStartRecursively(anim); |
| } |
| } |
| } |
| |
| public void dispatchOnCancel() { |
| dispatchOnCancelRecursively(mAnim); |
| } |
| |
| private void dispatchOnCancelRecursively(Animator animator) { |
| for (AnimatorListener l : nonNullList(animator.getListeners())) { |
| l.onAnimationCancel(animator); |
| } |
| |
| if (animator instanceof AnimatorSet) { |
| for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) { |
| dispatchOnCancelRecursively(anim); |
| } |
| } |
| } |
| |
| public void dispatchSetInterpolator(TimeInterpolator interpolator) { |
| dispatchSetInterpolatorRecursively(mAnim, interpolator); |
| } |
| |
| private void dispatchSetInterpolatorRecursively(Animator anim, TimeInterpolator interpolator) { |
| anim.setInterpolator(interpolator); |
| if (anim instanceof AnimatorSet) { |
| for (Animator child : nonNullList(((AnimatorSet) anim).getChildAnimations())) { |
| dispatchSetInterpolatorRecursively(child, interpolator); |
| } |
| } |
| } |
| |
| public void setOnCancelRunnable(Runnable runnable) { |
| mOnCancelRunnable = runnable; |
| } |
| |
| public Runnable getOnCancelRunnable() { |
| return mOnCancelRunnable; |
| } |
| |
| public void skipToEnd() { |
| mSkipToEnd = true; |
| for (SpringAnimation spring : mSprings) { |
| if (spring.canSkipToEnd()) { |
| spring.skipToEnd(); |
| } |
| } |
| mAnimationPlayer.end(); |
| mSkipToEnd = false; |
| } |
| |
| public static class AnimatorPlaybackControllerVL extends AnimatorPlaybackController { |
| |
| private final ValueAnimator[] mChildAnimations; |
| |
| private AnimatorPlaybackControllerVL(AnimatorSet anim, long duration, |
| Runnable onCancelRunnable) { |
| super(anim, duration, onCancelRunnable); |
| |
| // Build animation list |
| ArrayList<ValueAnimator> childAnims = new ArrayList<>(); |
| getAnimationsRecur(mAnim, childAnims); |
| mChildAnimations = childAnims.toArray(new ValueAnimator[childAnims.size()]); |
| } |
| |
| private void getAnimationsRecur(AnimatorSet anim, ArrayList<ValueAnimator> out) { |
| long forceDuration = anim.getDuration(); |
| TimeInterpolator forceInterpolator = anim.getInterpolator(); |
| for (Animator child : anim.getChildAnimations()) { |
| if (forceDuration > 0) { |
| child.setDuration(forceDuration); |
| } |
| if (forceInterpolator != null) { |
| child.setInterpolator(forceInterpolator); |
| } |
| if (child instanceof ValueAnimator) { |
| out.add((ValueAnimator) child); |
| } else if (child instanceof AnimatorSet) { |
| getAnimationsRecur((AnimatorSet) child, out); |
| } else { |
| throw new RuntimeException("Unknown animation type " + child); |
| } |
| } |
| } |
| |
| @Override |
| 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; |
| } |
| long playPos = clampDuration(fraction); |
| for (ValueAnimator anim : mChildAnimations) { |
| anim.setCurrentPlayTime(Math.min(playPos, anim.getDuration())); |
| } |
| } |
| } |
| |
| private boolean isAnySpringRunning() { |
| for (SpringAnimation spring : mSprings) { |
| if (spring.isRunning()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Only dispatches the on end actions once the animator and all springs have completed running. |
| */ |
| private class OnAnimationEndDispatcher extends AnimationSuccessListener { |
| |
| boolean mAnimatorDone = false; |
| boolean mSpringsDone = false; |
| boolean mDispatched = false; |
| |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mCancelled = false; |
| mDispatched = false; |
| } |
| |
| @Override |
| public void onAnimationSuccess(Animator animator) { |
| if (mSprings.isEmpty()) { |
| mSpringsDone = mAnimatorDone = true; |
| } |
| if (isAnySpringRunning()) { |
| mAnimatorDone = true; |
| } else { |
| mSpringsDone = true; |
| } |
| |
| // We wait for the spring (if any) to finish running before completing the end callback. |
| if (!mDispatched && (mSkipToEnd || (mAnimatorDone && mSpringsDone))) { |
| dispatchOnEndRecursively(mAnim); |
| if (mEndAction != null) { |
| mEndAction.run(); |
| } |
| mDispatched = true; |
| } |
| } |
| |
| private void dispatchOnEndRecursively(Animator animator) { |
| for (AnimatorListener l : nonNullList(animator.getListeners())) { |
| l.onAnimationEnd(animator); |
| } |
| |
| if (animator instanceof AnimatorSet) { |
| for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) { |
| dispatchOnEndRecursively(anim); |
| } |
| } |
| } |
| } |
| |
| private static <T> List<T> nonNullList(ArrayList<T> list) { |
| return list == null ? Collections.emptyList() : list; |
| } |
| } |