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.HANDLE_STATE_APPLY;
23 import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
24 
25 import android.animation.Animator;
26 import android.animation.Animator.AnimatorListener;
27 import android.animation.AnimatorListenerAdapter;
28 import android.animation.AnimatorSet;
29 import android.content.Context;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.util.Log;
33 
34 import androidx.annotation.FloatRange;
35 
36 import com.android.launcher3.anim.AnimationSuccessListener;
37 import com.android.launcher3.anim.AnimatorPlaybackController;
38 import com.android.launcher3.anim.PendingAnimation;
39 import com.android.launcher3.states.StateAnimationConfig;
40 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
41 import com.android.launcher3.states.StateAnimationConfig.AnimationPropertyFlags;
42 
43 import java.io.PrintWriter;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.stream.Collectors;
47 
48 /**
49  * Class to manage transitions between different states for a StatefulActivity based on different
50  * states
51  * @param STATE_TYPE Basestate used by the state manager
52  * @param STATEFUL_CONTAINER container object used to manage state
53  */
54 public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>,
55         STATEFUL_CONTAINER extends Context & StatefulContainer<STATE_TYPE>> {
56 
57     public static final String TAG = "StateManager";
58     // b/279059025, b/325463989
59     private static final boolean DEBUG = true;
60 
61     private final AnimationState mConfig = new AnimationState();
62     private final Handler mUiHandler;
63     private final STATEFUL_CONTAINER mStatefulContainer;
64     private final ArrayList<StateListener<STATE_TYPE>> mListeners = new ArrayList<>();
65     private final STATE_TYPE mBaseState;
66 
67     // Animators which are run on properties also controlled by state animations.
68     private final AtomicAnimationFactory mAtomicAnimationFactory;
69 
70     private StateHandler<STATE_TYPE>[] mStateHandlers;
71     private STATE_TYPE mState;
72 
73     private STATE_TYPE mLastStableState;
74     private STATE_TYPE mCurrentStableState;
75 
76     private STATE_TYPE mRestState;
77 
StateManager(STATEFUL_CONTAINER container, STATE_TYPE baseState)78     public StateManager(STATEFUL_CONTAINER container, STATE_TYPE baseState) {
79         mUiHandler = new Handler(Looper.getMainLooper());
80         mStatefulContainer = container;
81         mBaseState = baseState;
82         mState = mLastStableState = mCurrentStableState = baseState;
83         mAtomicAnimationFactory = container.createAtomicAnimationFactory();
84     }
85 
getState()86     public STATE_TYPE getState() {
87         return mState;
88     }
89 
getTargetState()90     public STATE_TYPE getTargetState() {
91         return (STATE_TYPE) mConfig.targetState;
92     }
93 
getCurrentStableState()94     public STATE_TYPE getCurrentStableState() {
95         return mCurrentStableState;
96     }
97 
98     @Override
toString()99     public String toString() {
100         return " StateManager(mLastStableState:" + mLastStableState
101                 + ", mCurrentStableState:" + mCurrentStableState
102                 + ", mState:" + mState
103                 + ", mRestState:" + mRestState
104                 + ", isInTransition:" + isInTransition() + ")";
105     }
106 
dump(String prefix, PrintWriter writer)107     public void dump(String prefix, PrintWriter writer) {
108         writer.println(prefix + "StateManager:");
109         writer.println(prefix + "\tmLastStableState:" + mLastStableState);
110         writer.println(prefix + "\tmCurrentStableState:" + mCurrentStableState);
111         writer.println(prefix + "\tmState:" + mState);
112         writer.println(prefix + "\tmRestState:" + mRestState);
113         writer.println(prefix + "\tisInTransition:" + isInTransition());
114     }
115 
getStateHandlers()116     public StateHandler<STATE_TYPE>[] getStateHandlers() {
117         if (mStateHandlers == null) {
118             ArrayList<StateHandler<STATE_TYPE>> handlers = new ArrayList<>();
119             mStatefulContainer.collectStateHandlers(handlers);
120             mStateHandlers = handlers.toArray(new StateHandler[handlers.size()]);
121         }
122         return mStateHandlers;
123     }
124 
addStateListener(StateListener listener)125     public void addStateListener(StateListener listener) {
126         mListeners.add(listener);
127     }
128 
removeStateListener(StateListener listener)129     public void removeStateListener(StateListener listener) {
130         mListeners.remove(listener);
131     }
132 
133     /**
134      * Returns true if the state changes should be animated.
135      */
shouldAnimateStateChange()136     public boolean shouldAnimateStateChange() {
137         return mStatefulContainer.shouldAnimateStateChange();
138     }
139 
140     /**
141      * @return {@code true} if the state matches the current state and there is no active
142      *         transition to different state.
143      */
isInStableState(STATE_TYPE state)144     public boolean isInStableState(STATE_TYPE state) {
145         return mState == state && mCurrentStableState == state
146                 && (mConfig.targetState == null || mConfig.targetState == state);
147     }
148 
149     /**
150      * @return {@code true} If there is an active transition.
151      */
isInTransition()152     public boolean isInTransition() {
153         return mConfig.currentAnimation != null;
154     }
155 
156     /**
157      * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
158      */
goToState(STATE_TYPE state)159     public void goToState(STATE_TYPE state) {
160         goToState(state, shouldAnimateStateChange());
161     }
162 
163     /**
164      * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
165      */
goToState(STATE_TYPE state, AnimatorListener listener)166     public void goToState(STATE_TYPE state, AnimatorListener listener) {
167         goToState(state, shouldAnimateStateChange(), listener);
168     }
169 
170     /**
171      * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
172      */
goToState(STATE_TYPE state, boolean animated)173     public void goToState(STATE_TYPE state, boolean animated) {
174         goToState(state, animated, 0, null);
175     }
176 
177     /**
178      * Changes the Launcher state to the provided state.
179      *
180      * @param animated false if the state should change immediately without any animation,
181      *                true otherwise
182      * @param listener any action to perform at the end of the transition, or null.
183      */
goToState(STATE_TYPE state, boolean animated, AnimatorListener listener)184     public void goToState(STATE_TYPE state, boolean animated, AnimatorListener listener) {
185         goToState(state, animated, 0, listener);
186     }
187 
188     /**
189      * Changes the Launcher state to the provided state after the given delay.
190      */
goToState(STATE_TYPE state, long delay, AnimatorListener listener)191     public void goToState(STATE_TYPE state, long delay, AnimatorListener listener) {
192         goToState(state, true, delay, listener);
193     }
194 
195     /**
196      * Changes the Launcher state to the provided state after the given delay.
197      */
goToState(STATE_TYPE state, long delay)198     public void goToState(STATE_TYPE state, long delay) {
199         goToState(state, true, delay, null);
200     }
201 
reapplyState()202     public void reapplyState() {
203         reapplyState(false);
204     }
205 
reapplyState(boolean cancelCurrentAnimation)206     public void reapplyState(boolean cancelCurrentAnimation) {
207         boolean wasInAnimation = mConfig.currentAnimation != null;
208         if (cancelCurrentAnimation && (mConfig.animProps & HANDLE_STATE_APPLY) == 0) {
209             // Animation canceling can trigger a cleanup routine, causing problems when we are in a
210             // launcher state that relies on member variable data. So if we are in one of those
211             // states, accelerate the current animation to its end point rather than canceling it
212             // outright.
213             if (mState.shouldPreserveDataStateOnReapply() && mConfig.currentAnimation != null) {
214                 mConfig.currentAnimation.end();
215             }
216             mAtomicAnimationFactory.cancelAllStateElementAnimation();
217             cancelAnimation();
218         }
219         if (mConfig.currentAnimation == null) {
220             for (StateHandler handler : getStateHandlers()) {
221                 handler.setState(mState);
222             }
223             if (wasInAnimation) {
224                 onStateTransitionEnd(mState);
225             }
226         }
227     }
228 
229     /** Handles backProgress in predictive back gesture by passing it to state handlers. */
onBackProgressed( STATE_TYPE toState, @FloatRange(from = 0.0, to = 1.0) float backProgress)230     public void onBackProgressed(
231             STATE_TYPE toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) {
232         for (StateHandler handler : getStateHandlers()) {
233             handler.onBackProgressed(toState, backProgress);
234         }
235     }
236 
237     /** Handles back cancelled event in predictive back gesture by passing it to state handlers. */
onBackCancelled(STATE_TYPE toState)238     public void onBackCancelled(STATE_TYPE toState) {
239         for (StateHandler handler : getStateHandlers()) {
240             handler.onBackCancelled(toState);
241         }
242     }
243 
goToState( STATE_TYPE state, boolean animated, long delay, AnimatorListener listener)244     private void goToState(
245             STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
246         if (DEBUG) {
247             Log.d(TAG, "goToState - fromState: " + mState + ", toState: " + state
248                     + ", partial trace:\n" + getTrimmedStackTrace("StateManager.goToState"));
249         }
250 
251         animated &= areAnimatorsEnabled();
252         if (mStatefulContainer.isInState(state)) {
253             if (mConfig.currentAnimation == null) {
254                 // Run any queued runnable
255                 if (listener != null) {
256                     listener.onAnimationEnd(null);
257                 }
258                 return;
259             } else if ((!mConfig.isUserControlled() && animated && mConfig.targetState == state)
260                     || mState.shouldPreserveDataStateOnReapply()) {
261                 // We are running the same animation as requested, and/or target state should not be
262                 // reset -- allow the current animation to complete instead of canceling it.
263                 if (listener != null) {
264                     mConfig.currentAnimation.addListener(listener);
265                 }
266                 return;
267             }
268         }
269 
270         // Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
271         STATE_TYPE fromState = mState;
272         cancelAnimation();
273 
274         if (!animated) {
275             mAtomicAnimationFactory.cancelAllStateElementAnimation();
276             onStateTransitionStart(state);
277             for (StateHandler handler : getStateHandlers()) {
278                 handler.setState(state);
279             }
280 
281             onStateTransitionEnd(state);
282 
283             // Run any queued runnable
284             if (listener != null) {
285                 listener.onAnimationEnd(null);
286             }
287             return;
288         }
289 
290         if (delay > 0) {
291             // Create the animation after the delay as some properties can change between preparing
292             // the animation and running the animation.
293             int startChangeId = mConfig.changeId;
294             mUiHandler.postDelayed(() -> {
295                 if (mConfig.changeId == startChangeId) {
296                     goToStateAnimated(state, fromState, listener);
297                 }
298             }, delay);
299         } else {
300             goToStateAnimated(state, fromState, listener);
301         }
302     }
303 
goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState, AnimatorListener listener)304     private void goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState,
305             AnimatorListener listener) {
306         // Since state mBaseState can be reached from multiple states, just assume that the
307         // transition plays in reverse and use the same duration as previous state.
308         mConfig.duration = state == mBaseState
309                 ? fromState.getTransitionDuration(mStatefulContainer, false /* isToState */)
310                 : state.getTransitionDuration(mStatefulContainer, true /* isToState */);
311         prepareForAtomicAnimation(fromState, state, mConfig);
312         AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
313         if (listener != null) {
314             animation.addListener(listener);
315         }
316         mUiHandler.post(new StartAnimRunnable(animation));
317     }
318 
319     /**
320      * Prepares for a non-user controlled animation from fromState to toState. Preparations include:
321      * - Setting interpolators for various animations included in the state transition.
322      * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
323      */
prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config)324     public void prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState,
325             StateAnimationConfig config) {
326         mAtomicAnimationFactory.prepareForAtomicAnimation(fromState, toState, config);
327     }
328 
329     /**
330      * Creates an animation representing atomic transitions between the provided states
331      */
createAtomicAnimation( STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config)332     public AnimatorSet createAtomicAnimation(
333             STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
334         if (DEBUG) {
335             Log.d(TAG, "createAtomicAnimation - fromState: " + fromState + ", toState: " + toState
336                     + ", partial trace:\n" + getTrimmedStackTrace(
337                             "StateManager.createAtomicAnimation"));
338         }
339 
340         PendingAnimation builder = new PendingAnimation(config.duration);
341         prepareForAtomicAnimation(fromState, toState, config);
342 
343         for (StateHandler handler : mStatefulContainer.getStateManager().getStateHandlers()) {
344             handler.setStateWithAnimation(toState, config, builder);
345         }
346         return builder.buildAnim();
347     }
348 
349     /**
350      * Creates a {@link AnimatorPlaybackController} that can be used for a controlled
351      * state transition.
352      * @param state the final state for the transition.
353      * @param duration intended duration for state playback. Use higher duration for better
354      *                accuracy.
355      */
createAnimationToNewWorkspace( STATE_TYPE state, long duration)356     public AnimatorPlaybackController createAnimationToNewWorkspace(
357             STATE_TYPE state, long duration) {
358         return createAnimationToNewWorkspace(state, duration, 0 /* animFlags */);
359     }
360 
createAnimationToNewWorkspace( STATE_TYPE state, long duration, @AnimationFlags int animFlags)361     public AnimatorPlaybackController createAnimationToNewWorkspace(
362             STATE_TYPE state, long duration, @AnimationFlags int animFlags) {
363         StateAnimationConfig config = new StateAnimationConfig();
364         config.duration = duration;
365         config.animFlags = animFlags;
366         return createAnimationToNewWorkspace(state, config);
367     }
368 
createAnimationToNewWorkspace(STATE_TYPE state, StateAnimationConfig config)369     public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
370             StateAnimationConfig config) {
371         config.animProps |= StateAnimationConfig.USER_CONTROLLED;
372         cancelAnimation();
373         config.copyTo(mConfig);
374         mConfig.playbackController = createAnimationToNewWorkspaceInternal(state)
375                 .createPlaybackController();
376         return mConfig.playbackController;
377     }
378 
createAnimationToNewWorkspaceInternal(final STATE_TYPE state)379     private PendingAnimation createAnimationToNewWorkspaceInternal(final STATE_TYPE state) {
380         PendingAnimation builder = new PendingAnimation(mConfig.duration);
381         if (!mConfig.hasAnimationFlag(SKIP_ALL_ANIMATIONS)) {
382             for (StateHandler handler : getStateHandlers()) {
383                 handler.setStateWithAnimation(state, mConfig, builder);
384             }
385         }
386         builder.addListener(createStateAnimationListener(state));
387         mConfig.setAnimation(builder.buildAnim(), state);
388         return builder;
389     }
390 
createStateAnimationListener(STATE_TYPE state)391     private AnimatorListener createStateAnimationListener(STATE_TYPE state) {
392         return new AnimationSuccessListener() {
393 
394             @Override
395             public void onAnimationStart(Animator animation) {
396                 // Change the internal state only when the transition actually starts
397                 onStateTransitionStart(state);
398             }
399 
400             @Override
401             public void onAnimationSuccess(Animator animator) {
402                 onStateTransitionEnd(state);
403             }
404         };
405     }
406 
407     private void onStateTransitionStart(STATE_TYPE state) {
408         mState = state;
409         mStatefulContainer.onStateSetStart(mState);
410 
411         if (DEBUG) {
412             Log.d(TAG, "onStateTransitionStart - state: " + state);
413         }
414         for (int i = mListeners.size() - 1; i >= 0; i--) {
415             mListeners.get(i).onStateTransitionStart(state);
416         }
417     }
418 
419     private void onStateTransitionEnd(STATE_TYPE state) {
420         // Only change the stable states after the transitions have finished
421         if (state != mCurrentStableState) {
422             mLastStableState = state.getHistoryForState(mCurrentStableState);
423             mCurrentStableState = state;
424         }
425 
426         mStatefulContainer.onStateSetEnd(state);
427         if (state == mBaseState) {
428             setRestState(null);
429         }
430 
431         if (DEBUG) {
432             Log.d(TAG, "onStateTransitionEnd - state: " + state);
433         }
434         for (int i = mListeners.size() - 1; i >= 0; i--) {
435             mListeners.get(i).onStateTransitionComplete(state);
436         }
437     }
438 
439     public STATE_TYPE getLastState() {
440         return mLastStableState;
441     }
442 
443     public void moveToRestState() {
444         moveToRestState(shouldAnimateStateChange());
445     }
446 
447     public void moveToRestState(boolean isAnimated) {
448         if (mConfig.currentAnimation != null && mConfig.isUserControlled()) {
449             // The user is doing something. Lets not mess it up
450             return;
451         }
452         if (mState.shouldDisableRestore()) {
453             goToState(getRestState(), isAnimated);
454             // Reset history
455             mLastStableState = mBaseState;
456         }
457     }
458 
459     public STATE_TYPE getRestState() {
460         return mRestState == null ? mBaseState : mRestState;
461     }
462 
463     public void setRestState(STATE_TYPE restState) {
464         mRestState = restState;
465     }
466 
467     /**
468      * Cancels the current animation.
469      */
470     public void cancelAnimation() {
471         if (DEBUG && mConfig.currentAnimation != null) {
472             Log.d(TAG, "cancelAnimation - with ongoing animation"
473                     + ", partial trace:\n" + getTrimmedStackTrace("StateManager.cancelAnimation"));
474         }
475         mConfig.reset();
476         // It could happen that a new animation is set as a result of an endListener on the
477         // existing animation.
478         while (mConfig.currentAnimation != null || mConfig.playbackController != null) {
479             mConfig.reset();
480         }
481     }
482 
483     /**
484      * Sets the provided controller as the current user controlled state animation
485      */
486     public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
487         setCurrentAnimation(controller, StateAnimationConfig.USER_CONTROLLED);
488     }
489 
490     public void setCurrentAnimation(AnimatorPlaybackController controller,
491             @AnimationPropertyFlags int animationProps) {
492         clearCurrentAnimation();
493         setCurrentAnimation(controller.getTarget());
494         mConfig.animProps = animationProps;
495         mConfig.playbackController = controller;
496     }
497 
498     /**
499      * @see #setCurrentAnimation(AnimatorSet, Animator...). Using this method tells the StateManager
500      * that this is a custom animation to the given state, and thus the StateManager will add an
501      * animation listener to call {@link #onStateTransitionStart} and {@link #onStateTransitionEnd}.
502      * @param anim The custom animation to the given state.
503      * @param toState The state we are animating towards.
504      */
505     public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) {
506         cancelAnimation();
507         setCurrentAnimation(anim);
508         anim.addListener(createStateAnimationListener(toState));
509     }
510 
511     /**
512      * Sets the animation as the current state animation, i.e., canceled when
513      * starting another animation and may block some launcher interactions while running.
514      *
515      * @param childAnimations Set of animations with the new target is controlling.
516      */
517     public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) {
518         for (Animator childAnim : childAnimations) {
519             if (childAnim == null) {
520                 continue;
521             }
522             if (mConfig.playbackController != null
523                     && mConfig.playbackController.getTarget() == childAnim) {
524                 clearCurrentAnimation();
525                 break;
526             } else if (mConfig.currentAnimation == childAnim) {
527                 clearCurrentAnimation();
528                 break;
529             }
530         }
531         boolean reapplyNeeded = mConfig.currentAnimation != null;
532         cancelAnimation();
533         if (reapplyNeeded) {
534             reapplyState();
535             // Dispatch on transition end, so that any transient property is cleared.
536             onStateTransitionEnd(mState);
537         }
538         mConfig.setAnimation(anim, null);
539     }
540 
541     /**
542      * Cancels a currently running gesture animation
543      */
544     public void cancelStateElementAnimation(int index) {
545         if (mAtomicAnimationFactory.mStateElementAnimators[index] != null) {
546             mAtomicAnimationFactory.mStateElementAnimators[index].cancel();
547         }
548     }
549 
550     public Animator createStateElementAnimation(int index, float... values) {
551         cancelStateElementAnimation(index);
552         Animator anim = mAtomicAnimationFactory.createStateElementAnimation(index, values);
553         mAtomicAnimationFactory.mStateElementAnimators[index] = anim;
554         anim.addListener(new AnimatorListenerAdapter() {
555             @Override
556             public void onAnimationEnd(Animator animation) {
557                 mAtomicAnimationFactory.mStateElementAnimators[index] = null;
558             }
559         });
560         return anim;
561     }
562 
563     private void clearCurrentAnimation() {
564         if (mConfig.currentAnimation != null) {
565             mConfig.currentAnimation.removeListener(mConfig);
566             mConfig.currentAnimation = null;
567         }
568         mConfig.playbackController = null;
569     }
570 
571     private String getTrimmedStackTrace(String callingMethodName) {
572         String stackTrace = Log.getStackTraceString(new Exception());
573         return Arrays.stream(stackTrace.split("\\n"))
574                 .skip(2) // Removes the line "java.lang.Exception" and "getTrimmedStackTrace".
575                 .filter(traceLine -> !traceLine.contains(callingMethodName))
576                 .limit(3)
577                 .collect(Collectors.joining("\n"));
578     }
579 
580     private class StartAnimRunnable implements Runnable {
581 
582         private final AnimatorSet mAnim;
583 
584         public StartAnimRunnable(AnimatorSet anim) {
585             mAnim = anim;
586         }
587 
588         @Override
589         public void run() {
590             if (mConfig.currentAnimation != mAnim) {
591                 return;
592             }
593             mAnim.start();
594         }
595     }
596 
597     private static class AnimationState<STATE_TYPE> extends StateAnimationConfig
598             implements AnimatorListener {
599 
600         private static final StateAnimationConfig DEFAULT = new StateAnimationConfig();
601 
602         public AnimatorPlaybackController playbackController;
603         public AnimatorSet currentAnimation;
604         public STATE_TYPE targetState;
605 
606         // Id to keep track of config changes, to tie an animation with the corresponding request
607         public int changeId = 0;
608 
609         /**
610          * Cancels the current animation and resets config variables.
611          */
612         public void reset() {
613             AnimatorSet anim = currentAnimation;
614             AnimatorPlaybackController pc = playbackController;
615 
616             DEFAULT.copyTo(this);
617             targetState = null;
618             currentAnimation = null;
619             playbackController = null;
620             changeId++;
621 
622             if (pc != null) {
623                 pc.getAnimationPlayer().cancel();
624                 pc.dispatchOnCancel().dispatchOnEnd();
625             } else if (anim != null) {
626                 anim.setDuration(0);
627                 if (!anim.isStarted()) {
628                     // If the animation is not started the listeners do not get notified,
629                     // notify manually.
630                     callListenerCommandRecursively(anim, AnimatorListener::onAnimationCancel);
631                     callListenerCommandRecursively(anim, AnimatorListener::onAnimationEnd);
632                 }
633                 anim.cancel();
634             }
635         }
636 
637         @Override
638         public void onAnimationEnd(Animator animation) {
639             if (playbackController != null && playbackController.getTarget() == animation) {
640                 playbackController = null;
641             }
642             if (currentAnimation == animation) {
643                 currentAnimation = null;
644             }
645         }
646 
647         public void setAnimation(AnimatorSet animation, STATE_TYPE targetState) {
648             currentAnimation = animation;
649             this.targetState = targetState;
650             currentAnimation.addListener(this);
651         }
652 
653         @Override
654         public void onAnimationStart(Animator animator) { }
655 
656         @Override
657         public void onAnimationCancel(Animator animator) { }
658 
659         @Override
660         public void onAnimationRepeat(Animator animator) { }
661     }
662 
663     public interface StateHandler<STATE_TYPE> {
664 
665         /**
666          * Updates the UI to {@param state} without any animations
667          */
668         void setState(STATE_TYPE state);
669 
670         /**
671          * Sets the UI to {@param state} by animating any changes.
672          */
673         void setStateWithAnimation(
674                 STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation);
675 
676         /** Handles backProgress in predictive back gesture for target state. */
677         default void onBackProgressed(
678                 STATE_TYPE toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) {};
679 
680         /** Handles back cancelled event in predictive back gesture for target state.  */
681         default void onBackCancelled(STATE_TYPE toState) {};
682     }
683 
684     public interface StateListener<STATE_TYPE> {
685 
686         default void onStateTransitionStart(STATE_TYPE toState) { }
687 
688         default void onStateTransitionComplete(STATE_TYPE finalState) { }
689     }
690 
691     /**
692      * Factory class to configure and create atomic animations.
693      */
694     public static class AtomicAnimationFactory<STATE_TYPE> {
695 
696         protected static final int NEXT_INDEX = 0;
697 
698         private final Animator[] mStateElementAnimators;
699 
700         /**
701          *
702          * @param sharedElementAnimCount number of animations which run on state properties
703          */
704         public AtomicAnimationFactory(int sharedElementAnimCount) {
705             mStateElementAnimators = new Animator[sharedElementAnimCount];
706         }
707 
708         void cancelAllStateElementAnimation() {
709             for (Animator animator : mStateElementAnimators) {
710                 if (animator != null) {
711                     animator.cancel();
712                 }
713             }
714         }
715 
716         /**
717          * Creates animations for elements which can be also be part of state transitions. The
718          * actual definition of the animation is up to the app to define.
719          *
720          */
721         public Animator createStateElementAnimation(int index, float... values) {
722             throw new RuntimeException("Unknown gesture animation " + index);
723         }
724 
725         /**
726          * Prepares for a non-user controlled animation from fromState to this state. Preparations
727          * include:
728          * - Setting interpolators for various animations included in the state transition.
729          * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
730          */
731         public void prepareForAtomicAnimation(
732                 STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) { }
733     }
734 }
735