Home | History | Annotate | Download | only in launcher3
      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