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; 18 19 import static android.view.View.VISIBLE; 20 import static com.android.launcher3.LauncherState.NORMAL; 21 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE; 22 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE; 23 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE; 24 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE; 25 import static com.android.launcher3.anim.Interpolators.ACCEL; 26 import static com.android.launcher3.anim.Interpolators.DEACCEL; 27 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7; 28 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; 29 import static com.android.launcher3.anim.Interpolators.clampToProgress; 30 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER; 31 32 import android.animation.Animator; 33 import android.animation.AnimatorListenerAdapter; 34 import android.animation.AnimatorSet; 35 import android.os.Handler; 36 import android.os.Looper; 37 import android.support.annotation.IntDef; 38 39 import com.android.launcher3.anim.AnimationSuccessListener; 40 import com.android.launcher3.anim.AnimatorPlaybackController; 41 import com.android.launcher3.anim.AnimatorSetBuilder; 42 import com.android.launcher3.anim.PropertySetter; 43 import com.android.launcher3.anim.PropertySetter.AnimatedPropertySetter; 44 import com.android.launcher3.uioverrides.UiFactory; 45 46 import java.lang.annotation.Retention; 47 import java.lang.annotation.RetentionPolicy; 48 import java.util.ArrayList; 49 50 /** 51 * TODO: figure out what kind of tests we can write for this 52 * 53 * Things to test when changing the following class. 54 * - Home from workspace 55 * - from center screen 56 * - from other screens 57 * - Home from all apps 58 * - from center screen 59 * - from other screens 60 * - Back from all apps 61 * - from center screen 62 * - from other screens 63 * - Launch app from workspace and quit 64 * - with back 65 * - with home 66 * - Launch app from all apps and quit 67 * - with back 68 * - with home 69 * - Go to a screen that's not the default, then all 70 * apps, and launch and app, and go back 71 * - with back 72 * -with home 73 * - On workspace, long press power and go back 74 * - with back 75 * - with home 76 * - On all apps, long press power and go back 77 * - with back 78 * - with home 79 * - On workspace, power off 80 * - On all apps, power off 81 * - Launch an app and turn off the screen while in that app 82 * - Go back with home key 83 * - Go back with back key TODO: make this not go to workspace 84 * - From all apps 85 * - From workspace 86 * - Enter and exit car mode (becase it causes an extra configuration changed) 87 * - From all apps 88 * - From the center workspace 89 * - From another workspace 90 */ 91 public class LauncherStateManager { 92 93 public static final String TAG = "StateManager"; 94 95 // We separate the state animations into "atomic" and "non-atomic" components. The atomic 96 // components may be run atomically - that is, all at once, instead of user-controlled. However, 97 // atomic components are not restricted to this purpose; they can be user-controlled alongside 98 // non atomic components as well. 99 @IntDef(flag = true, value = { 100 NON_ATOMIC_COMPONENT, 101 ATOMIC_COMPONENT 102 }) 103 @Retention(RetentionPolicy.SOURCE) 104 public @interface AnimationComponents {} 105 public static final int NON_ATOMIC_COMPONENT = 1 << 0; 106 public static final int ATOMIC_COMPONENT = 1 << 1; 107 108 public static final int ANIM_ALL = NON_ATOMIC_COMPONENT | ATOMIC_COMPONENT; 109 110 private final AnimationConfig mConfig = new AnimationConfig(); 111 private final Handler mUiHandler; 112 private final Launcher mLauncher; 113 private final ArrayList<StateListener> mListeners = new ArrayList<>(); 114 115 private StateHandler[] mStateHandlers; 116 private LauncherState mState = NORMAL; 117 118 private LauncherState mLastStableState = NORMAL; 119 private LauncherState mCurrentStableState = NORMAL; 120 121 private LauncherState mRestState; 122 123 public LauncherStateManager(Launcher l) { 124 mUiHandler = new Handler(Looper.getMainLooper()); 125 mLauncher = l; 126 } 127 128 public LauncherState getState() { 129 return mState; 130 } 131 132 public StateHandler[] getStateHandlers() { 133 if (mStateHandlers == null) { 134 mStateHandlers = UiFactory.getStateHandler(mLauncher); 135 } 136 return mStateHandlers; 137 } 138 139 public void addStateListener(StateListener listener) { 140 mListeners.add(listener); 141 } 142 143 public void removeStateListener(StateListener listener) { 144 mListeners.remove(listener); 145 } 146 147 /** 148 * @see #goToState(LauncherState, boolean, Runnable) 149 */ 150 public void goToState(LauncherState state) { 151 goToState(state, !mLauncher.isForceInvisible() && mLauncher.isStarted() /* animated */); 152 } 153 154 /** 155 * @see #goToState(LauncherState, boolean, Runnable) 156 */ 157 public void goToState(LauncherState state, boolean animated) { 158 goToState(state, animated, 0, null); 159 } 160 161 /** 162 * Changes the Launcher state to the provided state. 163 * 164 * @param animated false if the state should change immediately without any animation, 165 * true otherwise 166 * @paras onCompleteRunnable any action to perform at the end of the transition, of null. 167 */ 168 public void goToState(LauncherState state, boolean animated, Runnable onCompleteRunnable) { 169 goToState(state, animated, 0, onCompleteRunnable); 170 } 171 172 /** 173 * Changes the Launcher state to the provided state after the given delay. 174 */ 175 public void goToState(LauncherState state, long delay, Runnable onCompleteRunnable) { 176 goToState(state, true, delay, onCompleteRunnable); 177 } 178 179 /** 180 * Changes the Launcher state to the provided state after the given delay. 181 */ 182 public void goToState(LauncherState state, long delay) { 183 goToState(state, true, delay, null); 184 } 185 186 public void reapplyState() { 187 reapplyState(false); 188 } 189 190 public void reapplyState(boolean cancelCurrentAnimation) { 191 if (cancelCurrentAnimation) { 192 cancelAnimation(); 193 } 194 if (mConfig.mCurrentAnimation == null) { 195 for (StateHandler handler : getStateHandlers()) { 196 handler.setState(mState); 197 } 198 } 199 } 200 201 private void goToState(LauncherState state, boolean animated, long delay, 202 final Runnable onCompleteRunnable) { 203 if (mLauncher.isInState(state)) { 204 if (mConfig.mCurrentAnimation == null) { 205 // Run any queued runnable 206 if (onCompleteRunnable != null) { 207 onCompleteRunnable.run(); 208 } 209 return; 210 } else if (!mConfig.userControlled && animated && mConfig.mTargetState == state) { 211 // We are running the same animation as requested 212 if (onCompleteRunnable != null) { 213 mConfig.mCurrentAnimation.addListener(new AnimationSuccessListener() { 214 @Override 215 public void onAnimationSuccess(Animator animator) { 216 onCompleteRunnable.run(); 217 } 218 }); 219 } 220 return; 221 } 222 } 223 224 // Cancel the current animation. This will reset mState to mCurrentStableState, so store it. 225 LauncherState fromState = mState; 226 mConfig.reset(); 227 228 if (!animated) { 229 onStateTransitionStart(state); 230 for (StateHandler handler : getStateHandlers()) { 231 handler.setState(state); 232 } 233 234 for (int i = mListeners.size() - 1; i >= 0; i--) { 235 mListeners.get(i).onStateSetImmediately(state); 236 } 237 onStateTransitionEnd(state); 238 239 // Run any queued runnable 240 if (onCompleteRunnable != null) { 241 onCompleteRunnable.run(); 242 } 243 return; 244 } 245 246 // Since state NORMAL 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 == NORMAL ? fromState.transitionDuration : state.transitionDuration; 249 250 AnimatorSetBuilder builder = new AnimatorSetBuilder(); 251 prepareForAtomicAnimation(fromState, state, builder); 252 AnimatorSet animation = createAnimationToNewWorkspaceInternal( 253 state, builder, onCompleteRunnable); 254 Runnable runnable = new StartAnimRunnable(animation); 255 if (delay > 0) { 256 mUiHandler.postDelayed(runnable, delay); 257 } else { 258 mUiHandler.post(runnable); 259 } 260 } 261 262 /** 263 * Prepares for a non-user controlled animation from fromState to toState. Preparations include: 264 * - Setting interpolators for various animations included in the state transition. 265 * - Setting some start values (e.g. scale) for views that are hidden but about to be shown. 266 */ 267 public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState, 268 AnimatorSetBuilder builder) { 269 if (fromState == NORMAL && toState.overviewUi) { 270 builder.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2); 271 builder.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2); 272 builder.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2); 273 builder.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2); 274 275 // Start from a higher overview scale, but only if we're invisible so we don't jump. 276 UiFactory.prepareToShowOverview(mLauncher); 277 } else if (fromState.overviewUi && toState == NORMAL) { 278 builder.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL); 279 builder.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL); 280 builder.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f)); 281 builder.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7); 282 Workspace workspace = mLauncher.getWorkspace(); 283 284 // Start from a higher workspace scale, but only if we're invisible so we don't jump. 285 boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE; 286 if (isWorkspaceVisible) { 287 CellLayout currentChild = (CellLayout) workspace.getChildAt( 288 workspace.getCurrentPage()); 289 isWorkspaceVisible = currentChild.getVisibility() == VISIBLE 290 && currentChild.getShortcutsAndWidgets().getAlpha() > 0; 291 } 292 if (!isWorkspaceVisible) { 293 workspace.setScaleX(0.92f); 294 workspace.setScaleY(0.92f); 295 } 296 } 297 } 298 299 /** 300 * Creates a {@link AnimatorPlaybackController} that can be used for a controlled 301 * state transition. 302 * @param state the final state for the transition. 303 * @param duration intended duration for normal playback. Use higher duration for better 304 * accuracy. 305 */ 306 public AnimatorPlaybackController createAnimationToNewWorkspace( 307 LauncherState state, long duration) { 308 return createAnimationToNewWorkspace(state, duration, LauncherStateManager.ANIM_ALL); 309 } 310 311 public AnimatorPlaybackController createAnimationToNewWorkspace( 312 LauncherState state, long duration, @AnimationComponents int animComponents) { 313 return createAnimationToNewWorkspace(state, new AnimatorSetBuilder(), duration, null, 314 animComponents); 315 } 316 317 public AnimatorPlaybackController createAnimationToNewWorkspace(LauncherState state, 318 AnimatorSetBuilder builder, long duration, Runnable onCancelRunnable, 319 @AnimationComponents int animComponents) { 320 mConfig.reset(); 321 mConfig.userControlled = true; 322 mConfig.animComponents = animComponents; 323 mConfig.duration = duration; 324 mConfig.playbackController = AnimatorPlaybackController.wrap( 325 createAnimationToNewWorkspaceInternal(state, builder, null), duration, 326 onCancelRunnable); 327 return mConfig.playbackController; 328 } 329 330 protected AnimatorSet createAnimationToNewWorkspaceInternal(final LauncherState state, 331 AnimatorSetBuilder builder, final Runnable onCompleteRunnable) { 332 333 for (StateHandler handler : getStateHandlers()) { 334 builder.startTag(handler); 335 handler.setStateWithAnimation(state, builder, mConfig); 336 } 337 338 final AnimatorSet animation = builder.build(); 339 animation.addListener(new AnimationSuccessListener() { 340 341 @Override 342 public void onAnimationStart(Animator animation) { 343 // Change the internal state only when the transition actually starts 344 onStateTransitionStart(state); 345 for (int i = mListeners.size() - 1; i >= 0; i--) { 346 mListeners.get(i).onStateTransitionStart(state); 347 } 348 } 349 350 @Override 351 public void onAnimationCancel(Animator animation) { 352 super.onAnimationCancel(animation); 353 mState = mCurrentStableState; 354 } 355 356 @Override 357 public void onAnimationSuccess(Animator animator) { 358 // Run any queued runnables 359 if (onCompleteRunnable != null) { 360 onCompleteRunnable.run(); 361 } 362 onStateTransitionEnd(state); 363 for (int i = mListeners.size() - 1; i >= 0; i--) { 364 mListeners.get(i).onStateTransitionComplete(state); 365 } 366 } 367 }); 368 mConfig.setAnimation(animation, state); 369 return mConfig.mCurrentAnimation; 370 } 371 372 private void onStateTransitionStart(LauncherState state) { 373 mState.onStateDisabled(mLauncher); 374 mState = state; 375 mState.onStateEnabled(mLauncher); 376 mLauncher.getAppWidgetHost().setResumed(state == LauncherState.NORMAL); 377 378 if (state.disablePageClipping) { 379 // Only disable clipping if needed, otherwise leave it as previous value. 380 mLauncher.getWorkspace().setClipChildren(false); 381 } 382 UiFactory.onLauncherStateOrResumeChanged(mLauncher); 383 } 384 385 private void onStateTransitionEnd(LauncherState state) { 386 // Only change the stable states after the transitions have finished 387 if (state != mCurrentStableState) { 388 mLastStableState = state.getHistoryForState(mCurrentStableState); 389 mCurrentStableState = state; 390 } 391 392 state.onStateTransitionEnd(mLauncher); 393 mLauncher.getWorkspace().setClipChildren(!state.disablePageClipping); 394 mLauncher.finishAutoCancelActionMode(); 395 396 if (state == NORMAL) { 397 setRestState(null); 398 } 399 400 UiFactory.onLauncherStateOrResumeChanged(mLauncher); 401 402 mLauncher.getDragLayer().requestFocus(); 403 } 404 405 public void onWindowFocusChanged() { 406 UiFactory.onLauncherStateOrFocusChanged(mLauncher); 407 } 408 409 public LauncherState getLastState() { 410 return mLastStableState; 411 } 412 413 public void moveToRestState() { 414 if (mConfig.mCurrentAnimation != null && mConfig.userControlled) { 415 // The user is doing something. Lets not mess it up 416 return; 417 } 418 if (mState.disableRestore) { 419 goToState(getRestState()); 420 // Reset history 421 mLastStableState = NORMAL; 422 } 423 } 424 425 public LauncherState getRestState() { 426 return mRestState == null ? NORMAL : mRestState; 427 } 428 429 public void setRestState(LauncherState restState) { 430 mRestState = restState; 431 } 432 433 /** 434 * Cancels the current animation. 435 */ 436 public void cancelAnimation() { 437 mConfig.reset(); 438 } 439 440 public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) { 441 setCurrentAnimation(controller.getTarget()); 442 mConfig.userControlled = true; 443 mConfig.playbackController = controller; 444 } 445 446 /** 447 * Sets the animation as the current state animation, i.e., canceled when 448 * starting another animation and may block some launcher interactions while running. 449 * 450 * @param childAnimations Set of animations with the new target is controlling. 451 */ 452 public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) { 453 for (Animator childAnim : childAnimations) { 454 if (childAnim == null) { 455 continue; 456 } 457 if (mConfig.playbackController != null 458 && mConfig.playbackController.getTarget() == childAnim) { 459 clearCurrentAnimation(); 460 break; 461 } else if (mConfig.mCurrentAnimation == childAnim) { 462 clearCurrentAnimation(); 463 break; 464 } 465 } 466 boolean reapplyNeeded = mConfig.mCurrentAnimation != null; 467 cancelAnimation(); 468 if (reapplyNeeded) { 469 reapplyState(); 470 } 471 mConfig.setAnimation(anim, null); 472 } 473 474 private void clearCurrentAnimation() { 475 if (mConfig.mCurrentAnimation != null) { 476 mConfig.mCurrentAnimation.removeListener(mConfig); 477 mConfig.mCurrentAnimation = null; 478 } 479 mConfig.playbackController = null; 480 } 481 482 private class StartAnimRunnable implements Runnable { 483 484 private final AnimatorSet mAnim; 485 486 public StartAnimRunnable(AnimatorSet anim) { 487 mAnim = anim; 488 } 489 490 @Override 491 public void run() { 492 if (mConfig.mCurrentAnimation != mAnim) { 493 return; 494 } 495 mAnim.start(); 496 } 497 } 498 499 public static class AnimationConfig extends AnimatorListenerAdapter { 500 public long duration; 501 public boolean userControlled; 502 public AnimatorPlaybackController playbackController; 503 public @AnimationComponents int animComponents = ANIM_ALL; 504 private PropertySetter mPropertySetter; 505 506 private AnimatorSet mCurrentAnimation; 507 private LauncherState mTargetState; 508 509 /** 510 * Cancels the current animation and resets config variables. 511 */ 512 public void reset() { 513 duration = 0; 514 userControlled = false; 515 animComponents = ANIM_ALL; 516 mPropertySetter = null; 517 mTargetState = null; 518 519 if (playbackController != null) { 520 playbackController.getAnimationPlayer().cancel(); 521 playbackController.dispatchOnCancel(); 522 } else if (mCurrentAnimation != null) { 523 mCurrentAnimation.setDuration(0); 524 mCurrentAnimation.cancel(); 525 } 526 527 mCurrentAnimation = null; 528 playbackController = null; 529 } 530 531 public PropertySetter getPropertySetter(AnimatorSetBuilder builder) { 532 if (mPropertySetter == null) { 533 mPropertySetter = duration == 0 ? NO_ANIM_PROPERTY_SETTER 534 : new AnimatedPropertySetter(duration, builder); 535 } 536 return mPropertySetter; 537 } 538 539 @Override 540 public void onAnimationEnd(Animator animation) { 541 if (mCurrentAnimation == animation) { 542 mCurrentAnimation = null; 543 } 544 } 545 546 public void setAnimation(AnimatorSet animation, LauncherState targetState) { 547 mCurrentAnimation = animation; 548 mTargetState = targetState; 549 mCurrentAnimation.addListener(this); 550 } 551 552 public boolean playAtomicComponent() { 553 return (animComponents & ATOMIC_COMPONENT) != 0; 554 } 555 556 public boolean playNonAtomicComponent() { 557 return (animComponents & NON_ATOMIC_COMPONENT) != 0; 558 } 559 } 560 561 public interface StateHandler { 562 563 /** 564 * Updates the UI to {@param state} without any animations 565 */ 566 void setState(LauncherState state); 567 568 /** 569 * Sets the UI to {@param state} by animating any changes. 570 */ 571 void setStateWithAnimation(LauncherState toState, 572 AnimatorSetBuilder builder, AnimationConfig config); 573 } 574 575 public interface StateListener { 576 577 /** 578 * Called when the state is set without an animation. 579 */ 580 void onStateSetImmediately(LauncherState state); 581 582 void onStateTransitionStart(LauncherState toState); 583 void onStateTransitionComplete(LauncherState finalState); 584 } 585 } 586