• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.DisplayController.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 com.android.launcher3.Utilities;
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 
76     /** package private */
AnimatorPlaybackController(AnimatorSet anim, long duration, ArrayList<Holder> childAnims)77     AnimatorPlaybackController(AnimatorSet anim, long duration, ArrayList<Holder> childAnims) {
78         mAnim = anim;
79         mDuration = duration;
80 
81         mAnimationPlayer = ValueAnimator.ofFloat(0, 1);
82         mAnimationPlayer.setInterpolator(LINEAR);
83         mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
84         mAnimationPlayer.addUpdateListener(this);
85 
86         mAnim.addListener(new AnimatorListenerAdapter() {
87             @Override
88             public void onAnimationCancel(Animator animation) {
89                 mTargetCancelled = true;
90             }
91 
92             @Override
93             public void onAnimationEnd(Animator animation) {
94                 mTargetCancelled = false;
95             }
96 
97             @Override
98             public void onAnimationStart(Animator animation) {
99                 mTargetCancelled = false;
100             }
101         });
102 
103         mChildAnimations = childAnims.toArray(new Holder[childAnims.size()]);
104     }
105 
getTarget()106     public AnimatorSet getTarget() {
107         return mAnim;
108     }
109 
getDuration()110     public long getDuration() {
111         return mDuration;
112     }
113 
getInterpolator()114     public TimeInterpolator getInterpolator() {
115         return mAnim.getInterpolator() != null ? mAnim.getInterpolator() : LINEAR;
116     }
117 
118     /**
119      * Starts playing the animation forward from current position.
120      */
start()121     public void start() {
122         mAnimationPlayer.setFloatValues(mCurrentFraction, 1);
123         mAnimationPlayer.setDuration(clampDuration(1 - mCurrentFraction));
124         mAnimationPlayer.start();
125     }
126 
127     /**
128      * Starts playing the animation backwards from current position
129      */
reverse()130     public void reverse() {
131         mAnimationPlayer.setFloatValues(mCurrentFraction, 0);
132         mAnimationPlayer.setDuration(clampDuration(mCurrentFraction));
133         mAnimationPlayer.start();
134     }
135 
136     /**
137      * Starts playing the animation with the provided velocity optionally playing any
138      * physics based animations.
139      * @param goingToEnd Whether we are going to the end (progress = 1) or not (progress = 0).
140      * @param velocityPxPerMs The velocity at which to start the animation, in pixels / millisecond.
141      * @param endDistance The distance (pixels) that the animation will travel from progress 0 to 1.
142      * @param animationDuration The duration of the non-physics based animation.
143      */
startWithVelocity(Context context, boolean goingToEnd, float velocityPxPerMs, float endDistance, long animationDuration)144     public void startWithVelocity(Context context, boolean goingToEnd,
145             float velocityPxPerMs, float endDistance, long animationDuration) {
146         float distanceInverse = 1 / Math.abs(endDistance);
147         float velocityProgressPerMs = velocityPxPerMs * distanceInverse;
148 
149         float oneFrameProgress = velocityProgressPerMs * getSingleFrameMs(context);
150         float nextFrameProgress = boundToRange(getProgressFraction()
151                 + oneFrameProgress, 0f, 1f);
152 
153         // Update setters for spring
154         int springFlag = goingToEnd
155                 ? SpringProperty.FLAG_CAN_SPRING_ON_END
156                 : SpringProperty.FLAG_CAN_SPRING_ON_START;
157 
158         long springDuration = animationDuration;
159         for (Holder h : mChildAnimations) {
160             if ((h.springProperty.flags & springFlag) != 0) {
161                 SpringAnimationBuilder s = new SpringAnimationBuilder(context)
162                         .setStartValue(mCurrentFraction)
163                         .setEndValue(goingToEnd ? 1 : 0)
164                         .setStartVelocity(velocityProgressPerMs)
165                         .setMinimumVisibleChange(distanceInverse)
166                         .setDampingRatio(h.springProperty.mDampingRatio)
167                         .setStiffness(h.springProperty.mStiffness)
168                         .computeParams();
169 
170                 long expectedDurationL = s.getDuration();
171                 springDuration = Math.max(expectedDurationL, springDuration);
172 
173                 float expectedDuration = expectedDurationL;
174                 h.mapper = (progress, globalEndProgress) -> {
175                     if (expectedDuration <= 0 || oneFrameProgress >= 1) {
176                         return 1;
177                     } else {
178                         // Start from one frame ahead of the current position.
179                         return Utilities.mapToRange(
180                                 mAnimationPlayer.getCurrentPlayTime() / expectedDuration,
181                                 0, 1,
182                                 Math.abs(oneFrameProgress), 1,
183                                 LINEAR);
184                     }
185                 };
186                 h.anim.setInterpolator(s::getInterpolatedValue);
187             }
188         }
189 
190         mAnimationPlayer.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
191 
192         if (springDuration <= animationDuration) {
193             mAnimationPlayer.setDuration(animationDuration);
194             mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocityPxPerMs));
195         } else {
196             // Since spring requires more time to run, we let the other animations play with
197             // current time and interpolation and by clamping the duration.
198             mAnimationPlayer.setDuration(springDuration);
199 
200             float cutOff = animationDuration / (float) springDuration;
201             mAnimationPlayer.setInterpolator(
202                     clampToProgress(scrollInterpolatorForVelocity(velocityPxPerMs), 0, cutOff));
203         }
204         mAnimationPlayer.start();
205     }
206 
207     /**
208      * Tries to finish the running animation if it is close to completion.
209      */
forceFinishIfCloseToEnd()210     public void forceFinishIfCloseToEnd() {
211         if (mAnimationPlayer.isRunning()
212                 && mAnimationPlayer.getAnimatedFraction() > ANIMATION_COMPLETE_THRESHOLD) {
213             mAnimationPlayer.end();
214         }
215     }
216 
217     /**
218      * Pauses the currently playing animation.
219      */
pause()220     public void pause() {
221         // Reset property setters
222         for (Holder h : mChildAnimations) {
223             h.reset();
224         }
225         mAnimationPlayer.cancel();
226     }
227 
228     /**
229      * Returns the underlying animation used for controlling the set.
230      */
getAnimationPlayer()231     public ValueAnimator getAnimationPlayer() {
232         return mAnimationPlayer;
233     }
234 
235     /**
236      * Sets the current animation position and updates all the child animators accordingly.
237      */
setPlayFraction(float fraction)238     public void setPlayFraction(float fraction) {
239         mCurrentFraction = fraction;
240         // Let the animator report the progress but don't apply the progress to child
241         // animations if it has been cancelled.
242         if (mTargetCancelled) {
243             return;
244         }
245         float progress = boundToRange(fraction, 0, 1);
246         for (Holder holder : mChildAnimations) {
247             holder.setProgress(progress);
248         }
249     }
250 
getProgressFraction()251     public float getProgressFraction() {
252         return mCurrentFraction;
253     }
254 
getInterpolatedProgress()255     public float getInterpolatedProgress() {
256         return getInterpolator().getInterpolation(mCurrentFraction);
257     }
258 
259     /**
260      * Sets the action to be called when the animation is completed. Also clears any
261      * previously set action.
262      */
setEndAction(Runnable runnable)263     public void setEndAction(Runnable runnable) {
264         mEndAction = runnable;
265     }
266 
267     @Override
onAnimationUpdate(ValueAnimator valueAnimator)268     public void onAnimationUpdate(ValueAnimator valueAnimator) {
269         setPlayFraction((float) valueAnimator.getAnimatedValue());
270     }
271 
clampDuration(float fraction)272     protected long clampDuration(float fraction) {
273         float playPos = mDuration * fraction;
274         if (playPos <= 0) {
275             return 0;
276         } else {
277             return Math.min((long) playPos, mDuration);
278         }
279     }
280 
dispatchOnStart()281     public AnimatorPlaybackController dispatchOnStart() {
282         callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart);
283         return this;
284     }
285 
dispatchOnCancel()286     public AnimatorPlaybackController dispatchOnCancel() {
287         callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationCancel);
288         return this;
289     }
290 
dispatchOnEnd()291     public AnimatorPlaybackController dispatchOnEnd() {
292         callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd);
293         return this;
294     }
295 
dispatchSetInterpolator(TimeInterpolator interpolator)296     public void dispatchSetInterpolator(TimeInterpolator interpolator) {
297         callAnimatorCommandRecursively(mAnim, a -> a.setInterpolator(interpolator));
298     }
299 
300     /**
301      * Recursively calls a command on all the listeners of the provided animation
302      */
callListenerCommandRecursively( Animator anim, BiConsumer<AnimatorListener, Animator> command)303     public static void callListenerCommandRecursively(
304             Animator anim, BiConsumer<AnimatorListener, Animator> command) {
305         callAnimatorCommandRecursively(anim, a-> {
306             for (AnimatorListener l : nonNullList(a.getListeners())) {
307                 command.accept(l, a);
308             }
309         });
310     }
311 
callAnimatorCommandRecursively(Animator anim, Consumer<Animator> command)312     private static void callAnimatorCommandRecursively(Animator anim, Consumer<Animator> command) {
313         command.accept(anim);
314         if (anim instanceof AnimatorSet) {
315             for (Animator child : nonNullList(((AnimatorSet) anim).getChildAnimations())) {
316                 callAnimatorCommandRecursively(child, command);
317             }
318         }
319     }
320 
321     /**
322      * Only dispatches the on end actions once the animator and all springs have completed running.
323      */
324     private class OnAnimationEndDispatcher extends AnimationSuccessListener {
325 
326         boolean mDispatched = false;
327 
328         @Override
onAnimationStart(Animator animation)329         public void onAnimationStart(Animator animation) {
330             mCancelled = false;
331             mDispatched = false;
332         }
333 
334         @Override
onAnimationSuccess(Animator animator)335         public void onAnimationSuccess(Animator animator) {
336             // We wait for the spring (if any) to finish running before completing the end callback.
337             if (!mDispatched) {
338                 dispatchOnEnd();
339                 if (mEndAction != null) {
340                     mEndAction.run();
341                 }
342                 mDispatched = true;
343             }
344         }
345     }
346 
nonNullList(ArrayList<T> list)347     private static <T> List<T> nonNullList(ArrayList<T> list) {
348         return list == null ? Collections.emptyList() : list;
349     }
350 
351     /**
352      * Interface for mapping progress to animation progress
353      */
354     private interface ProgressMapper {
355 
356         ProgressMapper DEFAULT = (progress, globalEndProgress) ->
357                 progress > globalEndProgress ? 1 : (progress / globalEndProgress);
358 
getProgress(float progress, float globalProgress)359         float getProgress(float progress, float globalProgress);
360     }
361 
362     /**
363      * Holder class for various child animations
364      */
365     static class Holder {
366 
367         public final ValueAnimator anim;
368 
369         public final SpringProperty springProperty;
370 
371         public final TimeInterpolator interpolator;
372 
373         public final float globalEndProgress;
374 
375         public ProgressMapper mapper;
376 
Holder(Animator anim, float globalDuration, SpringProperty springProperty)377         Holder(Animator anim, float globalDuration, SpringProperty springProperty) {
378             this.anim = (ValueAnimator) anim;
379             this.springProperty = springProperty;
380             this.interpolator = this.anim.getInterpolator();
381             this.globalEndProgress = anim.getDuration() / globalDuration;
382             this.mapper = ProgressMapper.DEFAULT;
383         }
384 
setProgress(float progress)385         public void setProgress(float progress) {
386             anim.setCurrentFraction(mapper.getProgress(progress, globalEndProgress));
387         }
388 
reset()389         public void reset() {
390             anim.setInterpolator(interpolator);
391             mapper = ProgressMapper.DEFAULT;
392         }
393     }
394 
addAnimationHoldersRecur(Animator anim, long globalDuration, SpringProperty springProperty, ArrayList<Holder> out)395     static void addAnimationHoldersRecur(Animator anim, long globalDuration,
396             SpringProperty springProperty, ArrayList<Holder> out) {
397         long forceDuration = anim.getDuration();
398         TimeInterpolator forceInterpolator = anim.getInterpolator();
399         if (anim instanceof ValueAnimator) {
400             out.add(new Holder(anim, globalDuration, springProperty));
401         } else if (anim instanceof AnimatorSet) {
402             for (Animator child : ((AnimatorSet) anim).getChildAnimations()) {
403                 if (forceDuration > 0) {
404                     child.setDuration(forceDuration);
405                 }
406                 if (forceInterpolator != null) {
407                     child.setInterpolator(forceInterpolator);
408                 }
409                 addAnimationHoldersRecur(child, globalDuration, springProperty, out);
410             }
411         } else {
412             throw new RuntimeException("Unknown animation type " + anim);
413         }
414     }
415 }
416