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