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