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