• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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