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:" + isInTransition() + ")";
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:" + isInTransition());
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      * @return {@code true} If there is an active transition.
134      */
isInTransition()135     public boolean isInTransition() {
136         return mConfig.currentAnimation != null;
137     }
138 
139     /**
140      * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
141      */
goToState(STATE_TYPE state)142     public void goToState(STATE_TYPE state) {
143         goToState(state, shouldAnimateStateChange());
144     }
145 
146     /**
147      * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
148      */
goToState(STATE_TYPE state, boolean animated)149     public void goToState(STATE_TYPE state, boolean animated) {
150         goToState(state, animated, 0, null);
151     }
152 
153     /**
154      * Changes the Launcher state to the provided state.
155      *
156      * @param animated false if the state should change immediately without any animation,
157      *                true otherwise
158      * @paras onCompleteRunnable any action to perform at the end of the transition, of null.
159      */
goToState(STATE_TYPE state, boolean animated, AnimatorListener listener)160     public void goToState(STATE_TYPE state, boolean animated, AnimatorListener listener) {
161         goToState(state, animated, 0, listener);
162     }
163 
164     /**
165      * Changes the Launcher state to the provided state after the given delay.
166      */
goToState(STATE_TYPE state, long delay, AnimatorListener listener)167     public void goToState(STATE_TYPE state, long delay, AnimatorListener listener) {
168         goToState(state, true, delay, listener);
169     }
170 
171     /**
172      * Changes the Launcher state to the provided state after the given delay.
173      */
goToState(STATE_TYPE state, long delay)174     public void goToState(STATE_TYPE state, long delay) {
175         goToState(state, true, delay, null);
176     }
177 
reapplyState()178     public void reapplyState() {
179         reapplyState(false);
180     }
181 
reapplyState(boolean cancelCurrentAnimation)182     public void reapplyState(boolean cancelCurrentAnimation) {
183         boolean wasInAnimation = mConfig.currentAnimation != null;
184         if (cancelCurrentAnimation) {
185             mAtomicAnimationFactory.cancelAllStateElementAnimation();
186             cancelAnimation();
187         }
188         if (mConfig.currentAnimation == null) {
189             for (StateHandler handler : getStateHandlers()) {
190                 handler.setState(mState);
191             }
192             if (wasInAnimation) {
193                 onStateTransitionEnd(mState);
194             }
195         }
196     }
197 
goToState( STATE_TYPE state, boolean animated, long delay, AnimatorListener listener)198     private void goToState(
199             STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
200         animated &= areAnimatorsEnabled();
201         if (mActivity.isInState(state)) {
202             if (mConfig.currentAnimation == null) {
203                 // Run any queued runnable
204                 if (listener != null) {
205                     listener.onAnimationEnd(null);
206                 }
207                 return;
208             } else if (!mConfig.userControlled && animated && mConfig.targetState == state) {
209                 // We are running the same animation as requested
210                 if (listener != null) {
211                     mConfig.currentAnimation.addListener(listener);
212                 }
213                 return;
214             }
215         }
216 
217         // Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
218         STATE_TYPE fromState = mState;
219         cancelAnimation();
220 
221         if (!animated) {
222             mAtomicAnimationFactory.cancelAllStateElementAnimation();
223             onStateTransitionStart(state);
224             for (StateHandler handler : getStateHandlers()) {
225                 handler.setState(state);
226             }
227 
228             onStateTransitionEnd(state);
229 
230             // Run any queued runnable
231             if (listener != null) {
232                 listener.onAnimationEnd(null);
233             }
234             return;
235         }
236 
237         if (delay > 0) {
238             // Create the animation after the delay as some properties can change between preparing
239             // the animation and running the animation.
240             int startChangeId = mConfig.changeId;
241             mUiHandler.postDelayed(() -> {
242                 if (mConfig.changeId == startChangeId) {
243                     goToStateAnimated(state, fromState, listener);
244                 }
245             }, delay);
246         } else {
247             goToStateAnimated(state, fromState, listener);
248         }
249     }
250 
goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState, AnimatorListener listener)251     private void goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState,
252             AnimatorListener listener) {
253         // Since state mBaseState can be reached from multiple states, just assume that the
254         // transition plays in reverse and use the same duration as previous state.
255         mConfig.duration = state == mBaseState
256                 ? fromState.getTransitionDuration(mActivity)
257                 : state.getTransitionDuration(mActivity);
258         prepareForAtomicAnimation(fromState, state, mConfig);
259         AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
260         if (listener != null) {
261             animation.addListener(listener);
262         }
263         mUiHandler.post(new StartAnimRunnable(animation));
264     }
265 
266     /**
267      * Prepares for a non-user controlled animation from fromState to toState. Preparations include:
268      * - Setting interpolators for various animations included in the state transition.
269      * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
270      */
prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config)271     public void prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState,
272             StateAnimationConfig config) {
273         mAtomicAnimationFactory.prepareForAtomicAnimation(fromState, toState, config);
274     }
275 
276     /**
277      * Creates an animation representing atomic transitions between the provided states
278      */
createAtomicAnimation( STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config)279     public AnimatorSet createAtomicAnimation(
280             STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
281         PendingAnimation builder = new PendingAnimation(config.duration);
282         prepareForAtomicAnimation(fromState, toState, config);
283 
284         for (StateHandler handler : mActivity.getStateManager().getStateHandlers()) {
285             handler.setStateWithAnimation(toState, config, builder);
286         }
287         return builder.buildAnim();
288     }
289 
290     /**
291      * Creates a {@link AnimatorPlaybackController} that can be used for a controlled
292      * state transition.
293      * @param state the final state for the transition.
294      * @param duration intended duration for state playback. Use higher duration for better
295      *                accuracy.
296      */
createAnimationToNewWorkspace( STATE_TYPE state, long duration)297     public AnimatorPlaybackController createAnimationToNewWorkspace(
298             STATE_TYPE state, long duration) {
299         return createAnimationToNewWorkspace(state, duration, 0 /* animFlags */);
300     }
301 
createAnimationToNewWorkspace( STATE_TYPE state, long duration, @AnimationFlags int animFlags)302     public AnimatorPlaybackController createAnimationToNewWorkspace(
303             STATE_TYPE state, long duration, @AnimationFlags int animFlags) {
304         StateAnimationConfig config = new StateAnimationConfig();
305         config.duration = duration;
306         config.animFlags = animFlags;
307         return createAnimationToNewWorkspace(state, config);
308     }
309 
createAnimationToNewWorkspace(STATE_TYPE state, StateAnimationConfig config)310     public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
311             StateAnimationConfig config) {
312         config.userControlled = true;
313         cancelAnimation();
314         config.copyTo(mConfig);
315         mConfig.playbackController = createAnimationToNewWorkspaceInternal(state)
316                 .createPlaybackController();
317         return mConfig.playbackController;
318     }
319 
createAnimationToNewWorkspaceInternal(final STATE_TYPE state)320     private PendingAnimation createAnimationToNewWorkspaceInternal(final STATE_TYPE state) {
321         PendingAnimation builder = new PendingAnimation(mConfig.duration);
322         if (!mConfig.hasAnimationFlag(SKIP_ALL_ANIMATIONS)) {
323             for (StateHandler handler : getStateHandlers()) {
324                 handler.setStateWithAnimation(state, mConfig, builder);
325             }
326         }
327         builder.addListener(createStateAnimationListener(state));
328         mConfig.setAnimation(builder.buildAnim(), state);
329         return builder;
330     }
331 
createStateAnimationListener(STATE_TYPE state)332     private AnimatorListener createStateAnimationListener(STATE_TYPE state) {
333         return new AnimationSuccessListener() {
334 
335             @Override
336             public void onAnimationStart(Animator animation) {
337                 // Change the internal state only when the transition actually starts
338                 onStateTransitionStart(state);
339             }
340 
341             @Override
342             public void onAnimationSuccess(Animator animator) {
343                 onStateTransitionEnd(state);
344             }
345         };
346     }
347 
348     private void onStateTransitionStart(STATE_TYPE state) {
349         mState = state;
350         mActivity.onStateSetStart(mState);
351 
352         for (int i = mListeners.size() - 1; i >= 0; i--) {
353             mListeners.get(i).onStateTransitionStart(state);
354         }
355     }
356 
357     private void onStateTransitionEnd(STATE_TYPE state) {
358         // Only change the stable states after the transitions have finished
359         if (state != mCurrentStableState) {
360             mLastStableState = state.getHistoryForState(mCurrentStableState);
361             mCurrentStableState = state;
362         }
363 
364         mActivity.onStateSetEnd(state);
365         if (state == mBaseState) {
366             setRestState(null);
367         }
368 
369         for (int i = mListeners.size() - 1; i >= 0; i--) {
370             mListeners.get(i).onStateTransitionComplete(state);
371         }
372     }
373 
374     public STATE_TYPE getLastState() {
375         return mLastStableState;
376     }
377 
378     public void moveToRestState() {
379         if (mConfig.currentAnimation != null && mConfig.userControlled) {
380             // The user is doing something. Lets not mess it up
381             return;
382         }
383         if (mState.shouldDisableRestore()) {
384             goToState(getRestState());
385             // Reset history
386             mLastStableState = mBaseState;
387         }
388     }
389 
390     public STATE_TYPE getRestState() {
391         return mRestState == null ? mBaseState : mRestState;
392     }
393 
394     public void setRestState(STATE_TYPE restState) {
395         mRestState = restState;
396     }
397 
398     /**
399      * Cancels the current animation.
400      */
401     public void cancelAnimation() {
402         mConfig.reset();
403         // It could happen that a new animation is set as a result of an endListener on the
404         // existing animation.
405         while (mConfig.currentAnimation != null || mConfig.playbackController != null) {
406             mConfig.reset();
407         }
408     }
409 
410     public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
411         clearCurrentAnimation();
412         setCurrentAnimation(controller.getTarget());
413         mConfig.userControlled = true;
414         mConfig.playbackController = controller;
415     }
416 
417     /**
418      * @see #setCurrentAnimation(AnimatorSet, Animator...). Using this method tells the StateManager
419      * that this is a custom animation to the given state, and thus the StateManager will add an
420      * animation listener to call {@link #onStateTransitionStart} and {@link #onStateTransitionEnd}.
421      * @param anim The custom animation to the given state.
422      * @param toState The state we are animating towards.
423      */
424     public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) {
425         cancelAnimation();
426         setCurrentAnimation(anim);
427         anim.addListener(createStateAnimationListener(toState));
428     }
429 
430     /**
431      * Sets the animation as the current state animation, i.e., canceled when
432      * starting another animation and may block some launcher interactions while running.
433      *
434      * @param childAnimations Set of animations with the new target is controlling.
435      */
436     public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) {
437         for (Animator childAnim : childAnimations) {
438             if (childAnim == null) {
439                 continue;
440             }
441             if (mConfig.playbackController != null
442                     && mConfig.playbackController.getTarget() == childAnim) {
443                 clearCurrentAnimation();
444                 break;
445             } else if (mConfig.currentAnimation == childAnim) {
446                 clearCurrentAnimation();
447                 break;
448             }
449         }
450         boolean reapplyNeeded = mConfig.currentAnimation != null;
451         cancelAnimation();
452         if (reapplyNeeded) {
453             reapplyState();
454             // Dispatch on transition end, so that any transient property is cleared.
455             onStateTransitionEnd(mState);
456         }
457         mConfig.setAnimation(anim, null);
458     }
459 
460     /**
461      * Cancels a currently running gesture animation
462      */
463     public void cancelStateElementAnimation(int index) {
464         if (mAtomicAnimationFactory.mStateElementAnimators[index] != null) {
465             mAtomicAnimationFactory.mStateElementAnimators[index].cancel();
466         }
467     }
468 
469     public Animator createStateElementAnimation(int index, float... values) {
470         cancelStateElementAnimation(index);
471         Animator anim = mAtomicAnimationFactory.createStateElementAnimation(index, values);
472         mAtomicAnimationFactory.mStateElementAnimators[index] = anim;
473         anim.addListener(new AnimatorListenerAdapter() {
474             @Override
475             public void onAnimationEnd(Animator animation) {
476                 mAtomicAnimationFactory.mStateElementAnimators[index] = null;
477             }
478         });
479         return anim;
480     }
481 
482     private void clearCurrentAnimation() {
483         if (mConfig.currentAnimation != null) {
484             mConfig.currentAnimation.removeListener(mConfig);
485             mConfig.currentAnimation = null;
486         }
487         mConfig.playbackController = null;
488     }
489 
490     private class StartAnimRunnable implements Runnable {
491 
492         private final AnimatorSet mAnim;
493 
494         public StartAnimRunnable(AnimatorSet anim) {
495             mAnim = anim;
496         }
497 
498         @Override
499         public void run() {
500             if (mConfig.currentAnimation != mAnim) {
501                 return;
502             }
503             mAnim.start();
504         }
505     }
506 
507     private static class AnimationState<STATE_TYPE> extends StateAnimationConfig
508             implements AnimatorListener {
509 
510         private static final StateAnimationConfig DEFAULT = new StateAnimationConfig();
511 
512         public AnimatorPlaybackController playbackController;
513         public AnimatorSet currentAnimation;
514         public STATE_TYPE targetState;
515 
516         // Id to keep track of config changes, to tie an animation with the corresponding request
517         public int changeId = 0;
518 
519         /**
520          * Cancels the current animation and resets config variables.
521          */
522         public void reset() {
523             AnimatorSet anim = currentAnimation;
524             AnimatorPlaybackController pc = playbackController;
525 
526             DEFAULT.copyTo(this);
527             targetState = null;
528             currentAnimation = null;
529             playbackController = null;
530             changeId++;
531 
532             if (pc != null) {
533                 pc.getAnimationPlayer().cancel();
534                 pc.dispatchOnCancel().dispatchOnEnd();
535             } else if (anim != null) {
536                 anim.setDuration(0);
537                 if (!anim.isStarted()) {
538                     // If the animation is not started the listeners do not get notified,
539                     // notify manually.
540                     callListenerCommandRecursively(anim, AnimatorListener::onAnimationCancel);
541                     callListenerCommandRecursively(anim, AnimatorListener::onAnimationEnd);
542                 }
543                 anim.cancel();
544             }
545         }
546 
547         @Override
548         public void onAnimationEnd(Animator animation) {
549             if (playbackController != null && playbackController.getTarget() == animation) {
550                 playbackController = null;
551             }
552             if (currentAnimation == animation) {
553                 currentAnimation = null;
554             }
555         }
556 
557         public void setAnimation(AnimatorSet animation, STATE_TYPE targetState) {
558             currentAnimation = animation;
559             this.targetState = targetState;
560             currentAnimation.addListener(this);
561         }
562 
563         @Override
564         public void onAnimationStart(Animator animator) { }
565 
566         @Override
567         public void onAnimationCancel(Animator animator) { }
568 
569         @Override
570         public void onAnimationRepeat(Animator animator) { }
571     }
572 
573     public interface StateHandler<STATE_TYPE> {
574 
575         /**
576          * Updates the UI to {@param state} without any animations
577          */
578         void setState(STATE_TYPE state);
579 
580         /**
581          * Sets the UI to {@param state} by animating any changes.
582          */
583         void setStateWithAnimation(
584                 STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation);
585     }
586 
587     public interface StateListener<STATE_TYPE> {
588 
589         default void onStateTransitionStart(STATE_TYPE toState) { }
590 
591         default void onStateTransitionComplete(STATE_TYPE finalState) { }
592     }
593 
594     /**
595      * Factory class to configure and create atomic animations.
596      */
597     public static class AtomicAnimationFactory<STATE_TYPE> {
598 
599         protected static final int NEXT_INDEX = 0;
600 
601         private final Animator[] mStateElementAnimators;
602 
603         /**
604          *
605          * @param sharedElementAnimCount number of animations which run on state properties
606          */
607         public AtomicAnimationFactory(int sharedElementAnimCount) {
608             mStateElementAnimators = new Animator[sharedElementAnimCount];
609         }
610 
611         void cancelAllStateElementAnimation() {
612             for (Animator animator : mStateElementAnimators) {
613                 if (animator != null) {
614                     animator.cancel();
615                 }
616             }
617         }
618 
619         /**
620          * Creates animations for elements which can be also be part of state transitions. The
621          * actual definition of the animation is up to the app to define.
622          *
623          */
624         public Animator createStateElementAnimation(int index, float... values) {
625             throw new RuntimeException("Unknown gesture animation " + index);
626         }
627 
628         /**
629          * Prepares for a non-user controlled animation from fromState to this state. Preparations
630          * include:
631          * - Setting interpolators for various animations included in the state transition.
632          * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
633          */
634         public void prepareForAtomicAnimation(
635                 STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) { }
636     }
637 }
638