1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.anim;
17 
18 import static com.android.launcher3.Utilities.boundToRange;
19 import static com.android.launcher3.anim.Interpolators.LINEAR;
20 import static com.android.launcher3.anim.Interpolators.clampToProgress;
21 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
22 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
23 
24 import android.animation.Animator;
25 import android.animation.Animator.AnimatorListener;
26 import android.animation.AnimatorListenerAdapter;
27 import android.animation.AnimatorSet;
28 import android.animation.TimeInterpolator;
29 import android.animation.ValueAnimator;
30 import android.content.Context;
31 
32 import androidx.annotation.Nullable;
33 
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.function.BiConsumer;
38 import java.util.function.Consumer;
39 
40 /**
41  * Helper class to control the playback of an {@link AnimatorSet}, with custom interpolators
42  * and durations.
43  *
44  * Note: The implementation does not support start delays on child animations or
45  * sequential playbacks.
46  */
47 public class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
48 
49     /**
50      * Creates an animation controller for the provided animation.
51      * The actual duration does not matter as the animation is manually controlled. It just
52      * needs to be larger than the total number of pixels so that we don't have jittering due
53      * to float (animation-fraction * total duration) to int conversion.
54      */
wrap(AnimatorSet anim, long duration)55     public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
56         ArrayList<Holder> childAnims = new ArrayList<>();
57         addAnimationHoldersRecur(anim, duration, SpringProperty.DEFAULT, childAnims);
58 
59         return new AnimatorPlaybackController(anim, duration, childAnims);
60     }
61 
62     // Progress factor after which an animation is considered almost completed.
63     private static final float ANIMATION_COMPLETE_THRESHOLD = 0.95f;
64 
65     private final ValueAnimator mAnimationPlayer;
66     private final long mDuration;
67 
68     private final AnimatorSet mAnim;
69     private final Holder[] mChildAnimations;
70 
71     protected float mCurrentFraction;
72     private Runnable mEndAction;
73 
74     protected boolean mTargetCancelled = false;
75     protected Runnable mOnCancelRunnable;
76 
77     /** package private */
AnimatorPlaybackController(AnimatorSet anim, long duration, ArrayList<Holder> childAnims)78     AnimatorPlaybackController(AnimatorSet anim, long duration, ArrayList<Holder> childAnims) {
79         mAnim = anim;
80         mDuration = duration;
81 
82         mAnimationPlayer = ValueAnimator.ofFloat(0, 1);
83         mAnimationPlayer.setInterpolator(LINEAR);
84         mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
85         mAnimationPlayer.addUpdateListener(this);
86 
87         mAnim.addListener(new AnimatorListenerAdapter() {
88             @Override
89             public void onAnimationCancel(Animator animation) {
90                 mTargetCancelled = true;
91                 if (mOnCancelRunnable != null) {
92                     mOnCancelRunnable.run();
93                     mOnCancelRunnable = null;
94                 }
95             }
96 
97             @Override
98             public void onAnimationEnd(Animator animation) {
99                 mTargetCancelled = false;
100                 mOnCancelRunnable = null;
101             }
102 
103             @Override
104             public void onAnimationStart(Animator animation) {
105                 mTargetCancelled = false;
106             }
107         });
108 
109         mChildAnimations = childAnims.toArray(new Holder[childAnims.size()]);
110     }
111 
getTarget()112     public AnimatorSet getTarget() {
113         return mAnim;
114     }
115 
getDuration()116     public long getDuration() {
117         return mDuration;
118     }
119 
getInterpolator()120     public TimeInterpolator getInterpolator() {
121         return mAnim.getInterpolator() != null ? mAnim.getInterpolator() : LINEAR;
122     }
123 
124     /**
125      * Starts playing the animation forward from current position.
126      */
start()127     public void start() {
128         mAnimationPlayer.setFloatValues(mCurrentFraction, 1);
129         mAnimationPlayer.setDuration(clampDuration(1 - mCurrentFraction));
130         mAnimationPlayer.start();
131     }
132 
133     /**
134      * Starts playing the animation backwards from current position
135      */
reverse()136     public void reverse() {
137         mAnimationPlayer.setFloatValues(mCurrentFraction, 0);
138         mAnimationPlayer.setDuration(clampDuration(mCurrentFraction));
139         mAnimationPlayer.start();
140     }
141 
142     /**
143      * Starts playing the animation with the provided velocity optionally playing any
144      * physics based animations
145      */
startWithVelocity(Context context, boolean goingToEnd, float velocity, float scale, long animationDuration)146     public void startWithVelocity(Context context, boolean goingToEnd,
147             float velocity, float scale, long animationDuration) {
148         float scaleInverse = 1 / Math.abs(scale);
149         float scaledVelocity = velocity * scaleInverse;
150 
151         float nextFrameProgress = boundToRange(getProgressFraction()
152                 + scaledVelocity * getSingleFrameMs(context), 0f, 1f);
153 
154         // Update setters for spring
155         int springFlag = goingToEnd
156                 ? SpringProperty.FLAG_CAN_SPRING_ON_END
157                 : SpringProperty.FLAG_CAN_SPRING_ON_START;
158 
159         long springDuration = animationDuration;
160         for (Holder h : mChildAnimations) {
161             if ((h.springProperty.flags & springFlag) != 0) {
162                 SpringAnimationBuilder s = new SpringAnimationBuilder(context)
163                         .setStartValue(mCurrentFraction)
164                         .setEndValue(goingToEnd ? 1 : 0)
165                         .setStartVelocity(scaledVelocity)
166                         .setMinimumVisibleChange(scaleInverse)
167                         .setDampingRatio(h.springProperty.mDampingRatio)
168                         .setStiffness(h.springProperty.mStiffness)
169                         .computeParams();
170 
171                 long expectedDurationL = s.getDuration();
172                 springDuration = Math.max(expectedDurationL, springDuration);
173 
174                 float expectedDuration = expectedDurationL;
175                 h.mapper = (progress, globalEndProgress) ->
176                         mAnimationPlayer.getCurrentPlayTime() / expectedDuration;
177                 h.anim.setInterpolator(s::getInterpolatedValue);
178             }
179         }
180 
181         mAnimationPlayer.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
182 
183         if (springDuration <= animationDuration) {
184             mAnimationPlayer.setDuration(animationDuration);
185             mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocity));
186         } else {
187             // Since spring requires more time to run, we let the other animations play with
188             // current time and interpolation and by clamping the duration.
189             mAnimationPlayer.setDuration(springDuration);
190 
191             float cutOff = animationDuration / (float) springDuration;
192             mAnimationPlayer.setInterpolator(
193                     clampToProgress(scrollInterpolatorForVelocity(velocity), 0, cutOff));
194         }
195         mAnimationPlayer.start();
196     }
197 
198     /**
199      * Tries to finish the running animation if it is close to completion.
200      */
forceFinishIfCloseToEnd()201     public void forceFinishIfCloseToEnd() {
202         if (mAnimationPlayer.isRunning()
203                 && mAnimationPlayer.getAnimatedFraction() > ANIMATION_COMPLETE_THRESHOLD) {
204             mAnimationPlayer.end();
205         }
206     }
207 
208     /**
209      * Pauses the currently playing animation.
210      */
pause()211     public void pause() {
212         // Reset property setters
213         for (Holder h : mChildAnimations) {
214             h.reset();
215         }
216         mAnimationPlayer.cancel();
217     }
218 
219     /**
220      * Returns the underlying animation used for controlling the set.
221      */
getAnimationPlayer()222     public ValueAnimator getAnimationPlayer() {
223         return mAnimationPlayer;
224     }
225 
226     /**
227      * Sets the current animation position and updates all the child animators accordingly.
228      */
setPlayFraction(float fraction)229     public void setPlayFraction(float fraction) {
230         mCurrentFraction = fraction;
231         // Let the animator report the progress but don't apply the progress to child
232         // animations if it has been cancelled.
233         if (mTargetCancelled) {
234             return;
235         }
236         float progress = boundToRange(fraction, 0, 1);
237         for (Holder holder : mChildAnimations) {
238             holder.setProgress(progress);
239         }
240     }
241 
getProgressFraction()242     public float getProgressFraction() {
243         return mCurrentFraction;
244     }
245 
getInterpolatedProgress()246     public float getInterpolatedProgress() {
247         return getInterpolator().getInterpolation(mCurrentFraction);
248     }
249 
250     /**
251      * Sets the action to be called when the animation is completed. Also clears any
252      * previously set action.
253      */
setEndAction(Runnable runnable)254     public void setEndAction(Runnable runnable) {
255         mEndAction = runnable;
256     }
257 
258     @Override
onAnimationUpdate(ValueAnimator valueAnimator)259     public void onAnimationUpdate(ValueAnimator valueAnimator) {
260         setPlayFraction((float) valueAnimator.getAnimatedValue());
261     }
262 
clampDuration(float fraction)263     protected long clampDuration(float fraction) {
264         float playPos = mDuration * fraction;
265         if (playPos <= 0) {
266             return 0;
267         } else {
268             return Math.min((long) playPos, mDuration);
269         }
270     }
271 
272     /** @see #dispatchOnCancelWithoutCancelRunnable(Runnable) */
dispatchOnCancelWithoutCancelRunnable()273     public void dispatchOnCancelWithoutCancelRunnable() {
274         dispatchOnCancelWithoutCancelRunnable(null);
275     }
276 
277     /**
278      * Sets mOnCancelRunnable = null before dispatching the cancel and restoring the runnable. This
279      * is intended to be used only if you need to cancel but want to defer cleaning up yourself.
280      * @param callback An optional callback to run after dispatching the cancel but before resetting
281      *                 the onCancelRunnable.
282      */
dispatchOnCancelWithoutCancelRunnable(@ullable Runnable callback)283     public void dispatchOnCancelWithoutCancelRunnable(@Nullable Runnable callback) {
284         Runnable onCancel = mOnCancelRunnable;
285         setOnCancelRunnable(null);
286         dispatchOnCancel();
287         if (callback != null) {
288             callback.run();
289         }
290         setOnCancelRunnable(onCancel);
291     }
292 
293 
setOnCancelRunnable(Runnable runnable)294     public AnimatorPlaybackController setOnCancelRunnable(Runnable runnable) {
295         mOnCancelRunnable = runnable;
296         return this;
297     }
298 
dispatchOnStart()299     public void dispatchOnStart() {
300         callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart);
301     }
302 
dispatchOnCancel()303     public void dispatchOnCancel() {
304         callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationCancel);
305     }
306 
dispatchSetInterpolator(TimeInterpolator interpolator)307     public void dispatchSetInterpolator(TimeInterpolator interpolator) {
308         callAnimatorCommandRecursively(mAnim, a -> a.setInterpolator(interpolator));
309     }
310 
callListenerCommandRecursively( Animator anim, BiConsumer<AnimatorListener, Animator> command)311     private static void callListenerCommandRecursively(
312             Animator anim, BiConsumer<AnimatorListener, Animator> command) {
313         callAnimatorCommandRecursively(anim, a-> {
314             for (AnimatorListener l : nonNullList(a.getListeners())) {
315                 command.accept(l, a);
316             }
317         });
318     }
319 
callAnimatorCommandRecursively(Animator anim, Consumer<Animator> command)320     private static void callAnimatorCommandRecursively(Animator anim, Consumer<Animator> command) {
321         command.accept(anim);
322         if (anim instanceof AnimatorSet) {
323             for (Animator child : nonNullList(((AnimatorSet) anim).getChildAnimations())) {
324                 callAnimatorCommandRecursively(child, command);
325             }
326         }
327     }
328 
329     /**
330      * Only dispatches the on end actions once the animator and all springs have completed running.
331      */
332     private class OnAnimationEndDispatcher extends AnimationSuccessListener {
333 
334         boolean mDispatched = false;
335 
336         @Override
onAnimationStart(Animator animation)337         public void onAnimationStart(Animator animation) {
338             mCancelled = false;
339             mDispatched = false;
340         }
341 
342         @Override
onAnimationSuccess(Animator animator)343         public void onAnimationSuccess(Animator animator) {
344             // We wait for the spring (if any) to finish running before completing the end callback.
345             if (!mDispatched) {
346                 callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd);
347                 if (mEndAction != null) {
348                     mEndAction.run();
349                 }
350                 mDispatched = true;
351             }
352         }
353     }
354 
nonNullList(ArrayList<T> list)355     private static <T> List<T> nonNullList(ArrayList<T> list) {
356         return list == null ? Collections.emptyList() : list;
357     }
358 
359     /**
360      * Interface for mapping progress to animation progress
361      */
362     private interface ProgressMapper {
363 
364         ProgressMapper DEFAULT = (progress, globalEndProgress) ->
365                 progress > globalEndProgress ? 1 : (progress / globalEndProgress);
366 
getProgress(float progress, float globalProgress)367         float getProgress(float progress, float globalProgress);
368     }
369 
370     /**
371      * Holder class for various child animations
372      */
373     static class Holder {
374 
375         public final ValueAnimator anim;
376 
377         public final SpringProperty springProperty;
378 
379         public final TimeInterpolator interpolator;
380 
381         public final float globalEndProgress;
382 
383         public ProgressMapper mapper;
384 
Holder(Animator anim, float globalDuration, SpringProperty springProperty)385         Holder(Animator anim, float globalDuration, SpringProperty springProperty) {
386             this.anim = (ValueAnimator) anim;
387             this.springProperty = springProperty;
388             this.interpolator = this.anim.getInterpolator();
389             this.globalEndProgress = anim.getDuration() / globalDuration;
390             this.mapper = ProgressMapper.DEFAULT;
391         }
392 
setProgress(float progress)393         public void setProgress(float progress) {
394             anim.setCurrentFraction(mapper.getProgress(progress, globalEndProgress));
395         }
396 
reset()397         public void reset() {
398             anim.setInterpolator(interpolator);
399             mapper = ProgressMapper.DEFAULT;
400         }
401     }
402 
addAnimationHoldersRecur(Animator anim, long globalDuration, SpringProperty springProperty, ArrayList<Holder> out)403     static void addAnimationHoldersRecur(Animator anim, long globalDuration,
404             SpringProperty springProperty, ArrayList<Holder> out) {
405         long forceDuration = anim.getDuration();
406         TimeInterpolator forceInterpolator = anim.getInterpolator();
407         if (anim instanceof ValueAnimator) {
408             out.add(new Holder(anim, globalDuration, springProperty));
409         } else if (anim instanceof AnimatorSet) {
410             for (Animator child : ((AnimatorSet) anim).getChildAnimations()) {
411                 if (forceDuration > 0) {
412                     child.setDuration(forceDuration);
413                 }
414                 if (forceInterpolator != null) {
415                     child.setInterpolator(forceInterpolator);
416                 }
417                 addAnimationHoldersRecur(child, globalDuration, springProperty, out);
418             }
419         } else {
420             throw new RuntimeException("Unknown animation type " + anim);
421         }
422     }
423 }
424