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