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