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