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