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(new AnimationSuccessListener() { 315 316 @Override 317 public void onAnimationStart(Animator animation) { 318 // Change the internal state only when the transition actually starts 319 onStateTransitionStart(state); 320 } 321 322 @Override 323 public void onAnimationSuccess(Animator animator) { 324 if (TestProtocol.sDebugTracing) { 325 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "onAnimationSuccess: " + state); 326 } 327 onStateTransitionEnd(state); 328 } 329 }); 330 mConfig.setAnimation(builder.buildAnim(), state); 331 return builder; 332 } 333 onStateTransitionStart(STATE_TYPE state)334 private void onStateTransitionStart(STATE_TYPE state) { 335 mState = state; 336 mActivity.onStateSetStart(mState); 337 338 for (int i = mListeners.size() - 1; i >= 0; i--) { 339 mListeners.get(i).onStateTransitionStart(state); 340 } 341 } 342 onStateTransitionEnd(STATE_TYPE state)343 private void onStateTransitionEnd(STATE_TYPE state) { 344 // Only change the stable states after the transitions have finished 345 if (state != mCurrentStableState) { 346 mLastStableState = state.getHistoryForState(mCurrentStableState); 347 mCurrentStableState = state; 348 } 349 350 mActivity.onStateSetEnd(state); 351 if (state == mBaseState) { 352 setRestState(null); 353 } 354 355 for (int i = mListeners.size() - 1; i >= 0; i--) { 356 mListeners.get(i).onStateTransitionComplete(state); 357 } 358 } 359 getLastState()360 public STATE_TYPE getLastState() { 361 return mLastStableState; 362 } 363 moveToRestState()364 public void moveToRestState() { 365 if (mConfig.currentAnimation != null && mConfig.userControlled) { 366 // The user is doing something. Lets not mess it up 367 return; 368 } 369 if (mState.shouldDisableRestore()) { 370 goToState(getRestState()); 371 // Reset history 372 mLastStableState = mBaseState; 373 } 374 } 375 getRestState()376 public STATE_TYPE getRestState() { 377 return mRestState == null ? mBaseState : mRestState; 378 } 379 setRestState(STATE_TYPE restState)380 public void setRestState(STATE_TYPE restState) { 381 mRestState = restState; 382 } 383 384 /** 385 * Cancels the current animation. 386 */ cancelAnimation()387 public void cancelAnimation() { 388 mConfig.reset(); 389 } 390 setCurrentUserControlledAnimation(AnimatorPlaybackController controller)391 public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) { 392 clearCurrentAnimation(); 393 setCurrentAnimation(controller.getTarget()); 394 mConfig.userControlled = true; 395 mConfig.playbackController = controller; 396 } 397 398 /** 399 * Sets the animation as the current state animation, i.e., canceled when 400 * starting another animation and may block some launcher interactions while running. 401 * 402 * @param childAnimations Set of animations with the new target is controlling. 403 */ setCurrentAnimation(AnimatorSet anim, Animator... childAnimations)404 public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) { 405 for (Animator childAnim : childAnimations) { 406 if (childAnim == null) { 407 continue; 408 } 409 if (mConfig.playbackController != null 410 && mConfig.playbackController.getTarget() == childAnim) { 411 clearCurrentAnimation(); 412 break; 413 } else if (mConfig.currentAnimation == childAnim) { 414 clearCurrentAnimation(); 415 break; 416 } 417 } 418 boolean reapplyNeeded = mConfig.currentAnimation != null; 419 cancelAnimation(); 420 if (reapplyNeeded) { 421 reapplyState(); 422 // Dispatch on transition end, so that any transient property is cleared. 423 onStateTransitionEnd(mState); 424 } 425 mConfig.setAnimation(anim, null); 426 } 427 428 /** 429 * Cancels a currently running gesture animation 430 */ cancelStateElementAnimation(int index)431 public void cancelStateElementAnimation(int index) { 432 if (mAtomicAnimationFactory.mStateElementAnimators[index] != null) { 433 mAtomicAnimationFactory.mStateElementAnimators[index].cancel(); 434 } 435 } 436 createStateElementAnimation(int index, float... values)437 public Animator createStateElementAnimation(int index, float... values) { 438 cancelStateElementAnimation(index); 439 Animator anim = mAtomicAnimationFactory.createStateElementAnimation(index, values); 440 mAtomicAnimationFactory.mStateElementAnimators[index] = anim; 441 anim.addListener(new AnimatorListenerAdapter() { 442 @Override 443 public void onAnimationEnd(Animator animation) { 444 mAtomicAnimationFactory.mStateElementAnimators[index] = null; 445 } 446 }); 447 return anim; 448 } 449 clearCurrentAnimation()450 private void clearCurrentAnimation() { 451 if (mConfig.currentAnimation != null) { 452 mConfig.currentAnimation.removeListener(mConfig); 453 mConfig.currentAnimation = null; 454 } 455 mConfig.playbackController = null; 456 } 457 458 private class StartAnimRunnable implements Runnable { 459 460 private final AnimatorSet mAnim; 461 StartAnimRunnable(AnimatorSet anim)462 public StartAnimRunnable(AnimatorSet anim) { 463 mAnim = anim; 464 } 465 466 @Override run()467 public void run() { 468 if (mConfig.currentAnimation != mAnim) { 469 return; 470 } 471 mAnim.start(); 472 } 473 } 474 475 private static class AnimationState<STATE_TYPE> extends StateAnimationConfig 476 implements AnimatorListener { 477 478 private static final StateAnimationConfig DEFAULT = new StateAnimationConfig(); 479 480 public AnimatorPlaybackController playbackController; 481 public AnimatorSet currentAnimation; 482 public STATE_TYPE targetState; 483 484 // Id to keep track of config changes, to tie an animation with the corresponding request 485 public int changeId = 0; 486 487 /** 488 * Cancels the current animation and resets config variables. 489 */ reset()490 public void reset() { 491 DEFAULT.copyTo(this); 492 targetState = null; 493 494 if (playbackController != null) { 495 playbackController.getAnimationPlayer().cancel(); 496 playbackController.dispatchOnCancel(); 497 } else if (currentAnimation != null) { 498 currentAnimation.setDuration(0); 499 currentAnimation.cancel(); 500 } 501 502 currentAnimation = null; 503 playbackController = null; 504 changeId++; 505 } 506 507 @Override onAnimationEnd(Animator animation)508 public void onAnimationEnd(Animator animation) { 509 if (playbackController != null && playbackController.getTarget() == animation) { 510 playbackController = null; 511 } 512 if (currentAnimation == animation) { 513 currentAnimation = null; 514 } 515 } 516 setAnimation(AnimatorSet animation, STATE_TYPE targetState)517 public void setAnimation(AnimatorSet animation, STATE_TYPE targetState) { 518 currentAnimation = animation; 519 this.targetState = targetState; 520 currentAnimation.addListener(this); 521 } 522 523 @Override onAnimationStart(Animator animator)524 public void onAnimationStart(Animator animator) { } 525 526 @Override onAnimationCancel(Animator animator)527 public void onAnimationCancel(Animator animator) { } 528 529 @Override onAnimationRepeat(Animator animator)530 public void onAnimationRepeat(Animator animator) { } 531 } 532 533 public interface StateHandler<STATE_TYPE> { 534 535 /** 536 * Updates the UI to {@param state} without any animations 537 */ setState(STATE_TYPE state)538 void setState(STATE_TYPE state); 539 540 /** 541 * Sets the UI to {@param state} by animating any changes. 542 */ setStateWithAnimation( STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation)543 void setStateWithAnimation( 544 STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation); 545 } 546 547 public interface StateListener<STATE_TYPE> { 548 onStateTransitionStart(STATE_TYPE toState)549 default void onStateTransitionStart(STATE_TYPE toState) { } 550 onStateTransitionComplete(STATE_TYPE finalState)551 default void onStateTransitionComplete(STATE_TYPE finalState) { } 552 } 553 554 /** 555 * Factory class to configure and create atomic animations. 556 */ 557 public static class AtomicAnimationFactory<STATE_TYPE> { 558 559 protected static final int NEXT_INDEX = 0; 560 561 private final Animator[] mStateElementAnimators; 562 563 /** 564 * 565 * @param sharedElementAnimCount number of animations which run on state properties 566 */ AtomicAnimationFactory(int sharedElementAnimCount)567 public AtomicAnimationFactory(int sharedElementAnimCount) { 568 mStateElementAnimators = new Animator[sharedElementAnimCount]; 569 } 570 cancelAllStateElementAnimation()571 void cancelAllStateElementAnimation() { 572 for (Animator animator : mStateElementAnimators) { 573 if (animator != null) { 574 animator.cancel(); 575 } 576 } 577 } 578 579 /** 580 * Creates animations for elements which can be also be part of state transitions. The 581 * actual definition of the animation is up to the app to define. 582 * 583 */ createStateElementAnimation(int index, float... values)584 public Animator createStateElementAnimation(int index, float... values) { 585 throw new RuntimeException("Unknown gesture animation " + index); 586 } 587 588 /** 589 * Prepares for a non-user controlled animation from fromState to this state. Preparations 590 * include: 591 * - Setting interpolators for various animations included in the state transition. 592 * - Setting some start values (e.g. scale) for views that are hidden but about to be shown. 593 */ prepareForAtomicAnimation( STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config)594 public void prepareForAtomicAnimation( 595 STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) { } 596 } 597 } 598