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