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