1 /* 2 * Copyright (C) 2015 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 17 package com.android.launcher3.statemanager; 18 19 import static android.animation.ValueAnimator.areAnimatorsEnabled; 20 21 import static com.android.launcher3.Flags.enableStateManagerProtoLog; 22 import static com.android.launcher3.anim.AnimatorPlaybackController.callListenerCommandRecursively; 23 import static com.android.launcher3.states.StateAnimationConfig.HANDLE_STATE_APPLY; 24 import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS; 25 26 import android.animation.Animator; 27 import android.animation.Animator.AnimatorListener; 28 import android.animation.AnimatorListenerAdapter; 29 import android.animation.AnimatorSet; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.util.Log; 33 34 import androidx.annotation.FloatRange; 35 36 import com.android.launcher3.anim.AnimationSuccessListener; 37 import com.android.launcher3.anim.AnimatorPlaybackController; 38 import com.android.launcher3.anim.PendingAnimation; 39 import com.android.launcher3.states.StateAnimationConfig; 40 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags; 41 import com.android.launcher3.states.StateAnimationConfig.AnimationPropertyFlags; 42 import com.android.launcher3.util.StateManagerProtoLogProxy; 43 44 import java.io.PrintWriter; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.stream.Collectors; 48 49 /** 50 * Class to manage transitions between different states for a StatefulActivity based on different 51 * states 52 * @param <S> Basestate used by the state manager 53 * @param <T> container object used to manage state 54 */ 55 public class StateManager<S extends BaseState<S>, T extends StatefulContainer<S>> { 56 57 public static final String TAG = "StateManager"; 58 // b/279059025, b/325463989 59 private static final boolean DEBUG = true; 60 61 private final AnimationState<S> mConfig = new AnimationState<>(); 62 private final Handler mUiHandler; 63 private final T mContainer; 64 private final ArrayList<StateListener<S>> mListeners = new ArrayList<>(); 65 private final S mBaseState; 66 67 // Animators which are run on properties also controlled by state animations. 68 private final AtomicAnimationFactory<S> mAtomicAnimationFactory; 69 70 private StateHandler<S>[] mStateHandlers; 71 private S mState; 72 73 private S mLastStableState; 74 private S mCurrentStableState; 75 76 private S mRestState; 77 StateManager(T container, S baseState)78 public StateManager(T container, S baseState) { 79 mUiHandler = new Handler(Looper.getMainLooper()); 80 mContainer = container; 81 mBaseState = baseState; 82 mState = mLastStableState = mCurrentStableState = baseState; 83 mAtomicAnimationFactory = container.createAtomicAnimationFactory(); 84 } 85 getState()86 public S getState() { 87 return mState; 88 } 89 getTargetState()90 public S getTargetState() { 91 return mConfig.targetState; 92 } 93 getCurrentStableState()94 public S getCurrentStableState() { 95 return mCurrentStableState; 96 } 97 98 @Override toString()99 public String toString() { 100 return " StateManager(mLastStableState:" + mLastStableState 101 + ", mCurrentStableState:" + mCurrentStableState 102 + ", mState:" + mState 103 + ", mRestState:" + mRestState 104 + ", isInTransition:" + isInTransition() + ")"; 105 } 106 dump(String prefix, PrintWriter writer)107 public void dump(String prefix, PrintWriter writer) { 108 writer.println(prefix + "StateManager:"); 109 writer.println(prefix + "\tmLastStableState:" + mLastStableState); 110 writer.println(prefix + "\tmCurrentStableState:" + mCurrentStableState); 111 writer.println(prefix + "\tmState:" + mState); 112 writer.println(prefix + "\tmRestState:" + mRestState); 113 writer.println(prefix + "\tisInTransition:" + isInTransition()); 114 } 115 getStateHandlers()116 public StateHandler<S>[] getStateHandlers() { 117 if (mStateHandlers == null) { 118 ArrayList<StateHandler<S>> handlers = new ArrayList<>(); 119 mContainer.collectStateHandlers(handlers); 120 mStateHandlers = handlers.toArray(new StateHandler[handlers.size()]); 121 } 122 return mStateHandlers; 123 } 124 addStateListener(StateListener<S> listener)125 public void addStateListener(StateListener<S> listener) { 126 mListeners.add(listener); 127 } 128 removeStateListener(StateListener<S> listener)129 public void removeStateListener(StateListener<S> listener) { 130 mListeners.remove(listener); 131 } 132 133 /** 134 * Returns true if the state changes should be animated. 135 */ shouldAnimateStateChange()136 public boolean shouldAnimateStateChange() { 137 return mContainer.shouldAnimateStateChange(); 138 } 139 140 /** 141 * @return {@code true} if the state matches the current state and there is no active 142 * transition to different state. 143 */ isInStableState(S state)144 public boolean isInStableState(S state) { 145 return mState == state && mCurrentStableState == state 146 && (mConfig.targetState == null || mConfig.targetState == state); 147 } 148 149 /** 150 * @return {@code true} If there is an active transition. 151 */ isInTransition()152 public boolean isInTransition() { 153 return mConfig.currentAnimation != null; 154 } 155 156 /** 157 * @see #goToState(S, boolean, AnimatorListener) 158 */ goToState(S state)159 public void goToState(S state) { 160 goToState(state, shouldAnimateStateChange()); 161 } 162 163 /** 164 * @see #goToState(S, boolean, AnimatorListener) 165 */ goToState(S state, AnimatorListener listener)166 public void goToState(S state, AnimatorListener listener) { 167 goToState(state, shouldAnimateStateChange(), listener); 168 } 169 170 /** 171 * @see #goToState(S, boolean, AnimatorListener) 172 */ goToState(S state, boolean animated)173 public void goToState(S state, boolean animated) { 174 goToState(state, animated, 0, null); 175 } 176 177 /** 178 * Changes the Launcher state to the provided state. 179 * 180 * @param animated false if the state should change immediately without any animation, 181 * true otherwise 182 * @param listener any action to perform at the end of the transition, or null. 183 */ goToState(S state, boolean animated, AnimatorListener listener)184 public void goToState(S state, boolean animated, AnimatorListener listener) { 185 goToState(state, animated, 0, listener); 186 } 187 188 /** 189 * Changes the Launcher state to the provided state after the given delay. 190 */ goToState(S state, long delay, AnimatorListener listener)191 public void goToState(S state, long delay, AnimatorListener listener) { 192 goToState(state, true, delay, listener); 193 } 194 195 /** 196 * Changes the Launcher state to the provided state after the given delay. 197 */ goToState(S state, long delay)198 public void goToState(S state, long delay) { 199 goToState(state, true, delay, null); 200 } 201 reapplyState()202 public void reapplyState() { 203 reapplyState(false); 204 } 205 reapplyState(boolean cancelCurrentAnimation)206 public void reapplyState(boolean cancelCurrentAnimation) { 207 boolean wasInAnimation = mConfig.currentAnimation != null; 208 if (cancelCurrentAnimation && (mConfig.animProps & HANDLE_STATE_APPLY) == 0) { 209 // Animation canceling can trigger a cleanup routine, causing problems when we are in a 210 // launcher state that relies on member variable data. So if we are in one of those 211 // states, accelerate the current animation to its end point rather than canceling it 212 // outright. 213 if (mState.shouldPreserveDataStateOnReapply() && mConfig.currentAnimation != null) { 214 mConfig.currentAnimation.end(); 215 } 216 mAtomicAnimationFactory.cancelAllStateElementAnimation(); 217 cancelAnimation(); 218 } 219 if (mConfig.currentAnimation == null) { 220 for (StateHandler<S> handler : getStateHandlers()) { 221 handler.setState(mState); 222 } 223 if (wasInAnimation) { 224 onStateTransitionEnd(mState); 225 } 226 } 227 } 228 229 /** Handles backProgress in predictive back gesture by passing it to state handlers. */ onBackProgressed( S toState, @FloatRange(from = 0.0, to = 1.0) float backProgress)230 public void onBackProgressed( 231 S toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) { 232 for (StateHandler<S> handler : getStateHandlers()) { 233 handler.onBackProgressed(toState, backProgress); 234 } 235 } 236 237 /** Handles back cancelled event in predictive back gesture by passing it to state handlers. */ onBackCancelled(S toState)238 public void onBackCancelled(S toState) { 239 for (StateHandler<S> handler : getStateHandlers()) { 240 handler.onBackCancelled(toState); 241 } 242 } 243 goToState( S state, boolean animated, long delay, AnimatorListener listener)244 private void goToState( 245 S state, boolean animated, long delay, AnimatorListener listener) { 246 if (enableStateManagerProtoLog()) { 247 StateManagerProtoLogProxy.logGoToState( 248 mState, state, getTrimmedStackTrace("StateManager.goToState")); 249 } else if (DEBUG) { 250 Log.d(TAG, "goToState - fromState: " + mState + ", toState: " + state 251 + ", partial trace:\n" + getTrimmedStackTrace("StateManager.goToState")); 252 } 253 254 animated &= areAnimatorsEnabled(); 255 if (getState() == state) { 256 if (mConfig.currentAnimation == null) { 257 // Run any queued runnable 258 if (listener != null) { 259 listener.onAnimationEnd(new AnimatorSet()); 260 } 261 return; 262 } else if ((!mConfig.isUserControlled() && animated && mConfig.targetState == state) 263 || mState.shouldPreserveDataStateOnReapply()) { 264 // We are running the same animation as requested, and/or target state should not be 265 // reset -- allow the current animation to complete instead of canceling it. 266 if (listener != null) { 267 mConfig.currentAnimation.addListener(listener); 268 } 269 return; 270 } 271 } 272 273 // Cancel the current animation. This will reset mState to mCurrentStableState, so store it. 274 S fromState = mState; 275 cancelAnimation(); 276 277 if (!animated) { 278 mAtomicAnimationFactory.cancelAllStateElementAnimation(); 279 onStateTransitionStart(state); 280 for (StateHandler<S> handler : getStateHandlers()) { 281 handler.setState(state); 282 } 283 284 onStateTransitionEnd(state); 285 286 // Run any queued runnable 287 if (listener != null) { 288 listener.onAnimationEnd(new AnimatorSet()); 289 } 290 return; 291 } 292 293 if (delay > 0) { 294 // Create the animation after the delay as some properties can change between preparing 295 // the animation and running the animation. 296 int startChangeId = mConfig.changeId; 297 mUiHandler.postDelayed(() -> { 298 if (mConfig.changeId == startChangeId) { 299 goToStateAnimated(state, fromState, listener); 300 } 301 }, delay); 302 } else { 303 goToStateAnimated(state, fromState, listener); 304 } 305 } 306 goToStateAnimated(S state, S fromState, AnimatorListener listener)307 private void goToStateAnimated(S state, S fromState, 308 AnimatorListener listener) { 309 // Since state mBaseState can be reached from multiple states, just assume that the 310 // transition plays in reverse and use the same duration as previous state. 311 mConfig.duration = state == mBaseState 312 ? fromState.getTransitionDuration(mContainer, false /* isToState */) 313 : state.getTransitionDuration(mContainer, true /* isToState */); 314 prepareForAtomicAnimation(fromState, state, mConfig); 315 AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim(); 316 if (listener != null) { 317 animation.addListener(listener); 318 } 319 mUiHandler.post(new StartAnimRunnable(animation)); 320 } 321 322 /** 323 * Prepares for a non-user controlled animation from fromState to toState. Preparations include: 324 * - Setting interpolators for various animations included in the state transition. 325 * - Setting some start values (e.g. scale) for views that are hidden but about to be shown. 326 */ prepareForAtomicAnimation(S fromState, S toState, StateAnimationConfig config)327 public void prepareForAtomicAnimation(S fromState, S toState, 328 StateAnimationConfig config) { 329 mAtomicAnimationFactory.prepareForAtomicAnimation(fromState, toState, config); 330 } 331 332 /** 333 * Creates an animation representing atomic transitions between the provided states 334 */ createAtomicAnimation( S fromState, S toState, StateAnimationConfig config)335 public AnimatorSet createAtomicAnimation( 336 S fromState, S toState, StateAnimationConfig config) { 337 if (enableStateManagerProtoLog()) { 338 StateManagerProtoLogProxy.logCreateAtomicAnimation( 339 mState, toState, getTrimmedStackTrace("StateManager.createAtomicAnimation")); 340 } else if (DEBUG) { 341 Log.d(TAG, "createAtomicAnimation - fromState: " + fromState + ", toState: " + toState 342 + ", partial trace:\n" + getTrimmedStackTrace( 343 "StateManager.createAtomicAnimation")); 344 } 345 346 PendingAnimation builder = new PendingAnimation(config.duration); 347 prepareForAtomicAnimation(fromState, toState, config); 348 349 for (StateHandler<S> handler : getStateHandlers()) { 350 handler.setStateWithAnimation(toState, config, builder); 351 } 352 return builder.buildAnim(); 353 } 354 355 /** 356 * Creates a {@link AnimatorPlaybackController} that can be used for a controlled 357 * state transition. 358 * @param state the final state for the transition. 359 * @param duration intended duration for state playback. Use higher duration for better 360 * accuracy. 361 */ createAnimationToNewWorkspace( S state, long duration)362 public AnimatorPlaybackController createAnimationToNewWorkspace( 363 S state, long duration) { 364 return createAnimationToNewWorkspace(state, duration, 0 /* animFlags */); 365 } 366 createAnimationToNewWorkspace( S state, long duration, @AnimationFlags int animFlags)367 public AnimatorPlaybackController createAnimationToNewWorkspace( 368 S state, long duration, @AnimationFlags int animFlags) { 369 StateAnimationConfig config = new StateAnimationConfig(); 370 config.duration = duration; 371 config.animFlags = animFlags; 372 return createAnimationToNewWorkspace(state, config); 373 } 374 createAnimationToNewWorkspace(S state, StateAnimationConfig config)375 public AnimatorPlaybackController createAnimationToNewWorkspace(S state, 376 StateAnimationConfig config) { 377 config.animProps |= StateAnimationConfig.USER_CONTROLLED; 378 cancelAnimation(); 379 config.copyTo(mConfig); 380 mConfig.playbackController = createAnimationToNewWorkspaceInternal(state) 381 .createPlaybackController(); 382 return mConfig.playbackController; 383 } 384 createAnimationToNewWorkspaceInternal(final S state)385 private PendingAnimation createAnimationToNewWorkspaceInternal(final S state) { 386 PendingAnimation builder = new PendingAnimation(mConfig.duration); 387 if (!mConfig.hasAnimationFlag(SKIP_ALL_ANIMATIONS)) { 388 for (StateHandler<S> handler : getStateHandlers()) { 389 handler.setStateWithAnimation(state, mConfig, builder); 390 } 391 } 392 builder.addListener(createStateAnimationListener(state)); 393 mConfig.setAnimation(builder.buildAnim(), state); 394 return builder; 395 } 396 createStateAnimationListener(S state)397 private AnimatorListener createStateAnimationListener(S state) { 398 return new AnimationSuccessListener() { 399 400 @Override 401 public void onAnimationStart(Animator animation) { 402 // Change the internal state only when the transition actually starts 403 onStateTransitionStart(state); 404 } 405 406 @Override 407 public void onAnimationSuccess(Animator animator) { 408 onStateTransitionEnd(state); 409 } 410 }; 411 } 412 413 private void onStateTransitionStart(S state) { 414 mState = state; 415 mContainer.onStateSetStart(mState); 416 417 if (enableStateManagerProtoLog()) { 418 StateManagerProtoLogProxy.logOnStateTransitionStart(state); 419 } else if (DEBUG) { 420 Log.d(TAG, "onStateTransitionStart - state: " + state); 421 } 422 for (int i = mListeners.size() - 1; i >= 0; i--) { 423 mListeners.get(i).onStateTransitionStart(state); 424 } 425 } 426 427 private void onStateTransitionEnd(S state) { 428 // Only change the stable states after the transitions have finished 429 if (state != mCurrentStableState) { 430 mLastStableState = state.getHistoryForState(mCurrentStableState); 431 mCurrentStableState = state; 432 } 433 434 mContainer.onStateSetEnd(state); 435 if (state == mBaseState) { 436 setRestState(null); 437 } 438 439 if (enableStateManagerProtoLog()) { 440 StateManagerProtoLogProxy.logOnStateTransitionEnd(state); 441 } else if (DEBUG) { 442 Log.d(TAG, "onStateTransitionEnd - state: " + state); 443 } 444 for (int i = mListeners.size() - 1; i >= 0; i--) { 445 mListeners.get(i).onStateTransitionComplete(state); 446 } 447 } 448 449 public S getLastState() { 450 return mLastStableState; 451 } 452 453 public void moveToRestState() { 454 moveToRestState(shouldAnimateStateChange()); 455 } 456 457 public void moveToRestState(boolean isAnimated) { 458 if (mConfig.currentAnimation != null && mConfig.isUserControlled()) { 459 // The user is doing something. Lets not mess it up 460 return; 461 } 462 if (mState.shouldDisableRestore()) { 463 goToState(getRestState(), isAnimated); 464 // Reset history 465 mLastStableState = mBaseState; 466 } 467 } 468 469 public S getRestState() { 470 return mRestState == null ? mBaseState : mRestState; 471 } 472 473 public void setRestState(S restState) { 474 mRestState = restState; 475 } 476 477 /** 478 * Cancels the current animation. 479 */ 480 public void cancelAnimation() { 481 if (enableStateManagerProtoLog()) { 482 StateManagerProtoLogProxy.logCancelAnimation( 483 mConfig.currentAnimation != null, 484 getTrimmedStackTrace("StateManager.cancelAnimation")); 485 } else if (DEBUG && mConfig.currentAnimation != null) { 486 Log.d(TAG, "cancelAnimation - with ongoing animation" 487 + ", partial trace:\n" + getTrimmedStackTrace("StateManager.cancelAnimation")); 488 } 489 mConfig.reset(); 490 // It could happen that a new animation is set as a result of an endListener on the 491 // existing animation. 492 while (mConfig.currentAnimation != null || mConfig.playbackController != null) { 493 mConfig.reset(); 494 } 495 } 496 497 /** 498 * Sets the provided controller as the current user controlled state animation 499 */ 500 public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) { 501 setCurrentAnimation(controller, StateAnimationConfig.USER_CONTROLLED); 502 } 503 504 public void setCurrentAnimation(AnimatorPlaybackController controller, 505 @AnimationPropertyFlags int animationProps) { 506 clearCurrentAnimation(); 507 setCurrentAnimation(controller.getTarget()); 508 mConfig.animProps = animationProps; 509 mConfig.playbackController = controller; 510 } 511 512 /** 513 * @see #setCurrentAnimation(AnimatorSet, Animator...). Using this method tells the StateManager 514 * that this is a custom animation to the given state, and thus the StateManager will add an 515 * animation listener to call {@link #onStateTransitionStart} and {@link #onStateTransitionEnd}. 516 * @param anim The custom animation to the given state. 517 * @param toState The state we are animating towards. 518 */ 519 public void setCurrentAnimation(AnimatorSet anim, S toState) { 520 cancelAnimation(); 521 setCurrentAnimation(anim); 522 anim.addListener(createStateAnimationListener(toState)); 523 } 524 525 /** 526 * Sets the animation as the current state animation, i.e., canceled when 527 * starting another animation and may block some launcher interactions while running. 528 * 529 * @param childAnimations Set of animations with the new target is controlling. 530 */ 531 public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) { 532 for (Animator childAnim : childAnimations) { 533 if (childAnim == null) { 534 continue; 535 } 536 if (mConfig.playbackController != null 537 && mConfig.playbackController.getTarget() == childAnim) { 538 clearCurrentAnimation(); 539 break; 540 } else if (mConfig.currentAnimation == childAnim) { 541 clearCurrentAnimation(); 542 break; 543 } 544 } 545 boolean reapplyNeeded = mConfig.currentAnimation != null; 546 cancelAnimation(); 547 if (reapplyNeeded) { 548 reapplyState(); 549 // Dispatch on transition end, so that any transient property is cleared. 550 onStateTransitionEnd(mState); 551 } 552 mConfig.setAnimation(anim, null); 553 } 554 555 /** 556 * Cancels a currently running gesture animation 557 */ 558 public void cancelStateElementAnimation(int index) { 559 if (mAtomicAnimationFactory.mStateElementAnimators[index] != null) { 560 mAtomicAnimationFactory.mStateElementAnimators[index].cancel(); 561 } 562 } 563 564 public Animator createStateElementAnimation(int index, float... values) { 565 cancelStateElementAnimation(index); 566 Animator anim = mAtomicAnimationFactory.createStateElementAnimation(index, values); 567 mAtomicAnimationFactory.mStateElementAnimators[index] = anim; 568 anim.addListener(new AnimatorListenerAdapter() { 569 @Override 570 public void onAnimationEnd(Animator animation) { 571 mAtomicAnimationFactory.mStateElementAnimators[index] = null; 572 } 573 }); 574 return anim; 575 } 576 577 private void clearCurrentAnimation() { 578 if (mConfig.currentAnimation != null) { 579 mConfig.currentAnimation.removeListener(mConfig); 580 mConfig.currentAnimation = null; 581 } 582 mConfig.playbackController = null; 583 } 584 585 private String getTrimmedStackTrace(String callingMethodName) { 586 String stackTrace = Log.getStackTraceString(new Exception()); 587 return Arrays.stream(stackTrace.split("\\n")) 588 .skip(2) // Removes the line "java.lang.Exception" and "getTrimmedStackTrace". 589 .filter(traceLine -> !traceLine.contains(callingMethodName)) 590 .limit(3) 591 .collect(Collectors.joining("\n")); 592 } 593 594 private class StartAnimRunnable implements Runnable { 595 596 private final AnimatorSet mAnim; 597 598 public StartAnimRunnable(AnimatorSet anim) { 599 mAnim = anim; 600 } 601 602 @Override 603 public void run() { 604 if (mConfig.currentAnimation != mAnim) { 605 return; 606 } 607 mAnim.start(); 608 } 609 } 610 611 private static class AnimationState<STATE_TYPE> extends StateAnimationConfig 612 implements AnimatorListener { 613 614 private static final StateAnimationConfig DEFAULT = new StateAnimationConfig(); 615 616 public AnimatorPlaybackController playbackController; 617 public AnimatorSet currentAnimation; 618 public STATE_TYPE targetState; 619 620 // Id to keep track of config changes, to tie an animation with the corresponding request 621 public int changeId = 0; 622 623 /** 624 * Cancels the current animation and resets config variables. 625 */ 626 public void reset() { 627 AnimatorSet anim = currentAnimation; 628 AnimatorPlaybackController pc = playbackController; 629 630 DEFAULT.copyTo(this); 631 targetState = null; 632 currentAnimation = null; 633 playbackController = null; 634 changeId++; 635 636 if (pc != null) { 637 pc.getAnimationPlayer().cancel(); 638 pc.dispatchOnCancel().dispatchOnEnd(); 639 } else if (anim != null) { 640 anim.setDuration(0); 641 if (!anim.isStarted()) { 642 // If the animation is not started the listeners do not get notified, 643 // notify manually. 644 callListenerCommandRecursively(anim, AnimatorListener::onAnimationCancel); 645 callListenerCommandRecursively(anim, AnimatorListener::onAnimationEnd); 646 } 647 anim.cancel(); 648 } 649 } 650 651 @Override 652 public void onAnimationEnd(Animator animation) { 653 if (playbackController != null && playbackController.getTarget() == animation) { 654 playbackController = null; 655 } 656 if (currentAnimation == animation) { 657 currentAnimation = null; 658 } 659 } 660 661 public void setAnimation(AnimatorSet animation, STATE_TYPE targetState) { 662 currentAnimation = animation; 663 this.targetState = targetState; 664 currentAnimation.addListener(this); 665 } 666 667 @Override 668 public void onAnimationStart(Animator animator) { } 669 670 @Override 671 public void onAnimationCancel(Animator animator) { } 672 673 @Override 674 public void onAnimationRepeat(Animator animator) { } 675 } 676 677 public interface StateHandler<STATE_TYPE> { 678 679 /** 680 * Updates the UI to {@param state} without any animations 681 */ 682 void setState(STATE_TYPE state); 683 684 /** 685 * Sets the UI to {@param state} by animating any changes. 686 */ 687 void setStateWithAnimation( 688 STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation); 689 690 /** Handles backProgress in predictive back gesture for target state. */ 691 default void onBackProgressed( 692 STATE_TYPE toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) {} 693 694 /** Handles back cancelled event in predictive back gesture for target state. */ 695 default void onBackCancelled(STATE_TYPE toState) {} 696 } 697 698 public interface StateListener<STATE_TYPE> { 699 700 default void onStateTransitionStart(STATE_TYPE toState) { } 701 702 default void onStateTransitionComplete(STATE_TYPE finalState) { } 703 } 704 705 /** 706 * Factory class to configure and create atomic animations. 707 */ 708 public static class AtomicAnimationFactory<STATE_TYPE> { 709 710 protected static final int NEXT_INDEX = 0; 711 712 private final Animator[] mStateElementAnimators; 713 714 /** 715 * 716 * @param sharedElementAnimCount number of animations which run on state properties 717 */ 718 public AtomicAnimationFactory(int sharedElementAnimCount) { 719 mStateElementAnimators = new Animator[sharedElementAnimCount]; 720 } 721 722 void cancelAllStateElementAnimation() { 723 for (Animator animator : mStateElementAnimators) { 724 if (animator != null) { 725 animator.cancel(); 726 } 727 } 728 } 729 730 /** 731 * Creates animations for elements which can be also be part of state transitions. The 732 * actual definition of the animation is up to the app to define. 733 * 734 */ 735 public Animator createStateElementAnimation(int index, float... values) { 736 throw new RuntimeException("Unknown gesture animation " + index); 737 } 738 739 /** 740 * Prepares for a non-user controlled animation from fromState to this state. Preparations 741 * include: 742 * - Setting interpolators for various animations included in the state transition. 743 * - Setting some start values (e.g. scale) for views that are hidden but about to be shown. 744 */ 745 public void prepareForAtomicAnimation( 746 STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) { } 747 } 748 } 749