1 /*
2  * Copyright (C) 2018 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 androidx.constraintlayout.motion.widget;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.content.res.XmlResourceParser;
23 import android.graphics.RectF;
24 import android.util.AttributeSet;
25 import android.util.Log;
26 import android.util.SparseArray;
27 import android.util.SparseIntArray;
28 import android.util.TypedValue;
29 import android.util.Xml;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.animation.AccelerateDecelerateInterpolator;
33 import android.view.animation.AccelerateInterpolator;
34 import android.view.animation.AnimationUtils;
35 import android.view.animation.AnticipateInterpolator;
36 import android.view.animation.BounceInterpolator;
37 import android.view.animation.DecelerateInterpolator;
38 import android.view.animation.Interpolator;
39 import android.view.animation.OvershootInterpolator;
40 
41 import androidx.constraintlayout.core.motion.utils.Easing;
42 import androidx.constraintlayout.widget.ConstraintSet;
43 import androidx.constraintlayout.widget.R;
44 import androidx.constraintlayout.widget.StateSet;
45 
46 import org.xmlpull.v1.XmlPullParser;
47 import org.xmlpull.v1.XmlPullParserException;
48 
49 import java.io.IOException;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55 
56 /**
57  * The information to transition between multiple ConstraintSets
58  * This Class is meant to be used from XML
59  *
60  */
61 public class MotionScene {
62     private static final String TAG = "MotionScene";
63     private static final boolean DEBUG = false;
64     private static final int MIN_DURATION = 8;
65     static final int TRANSITION_BACKWARD = 0;
66     static final int TRANSITION_FORWARD = 1;
67     private static final int SPLINE_STRING = -1;
68     private static final int INTERPOLATOR_REFERENCE_ID = -2;
69     public static final int UNSET = -1;
70     private final MotionLayout mMotionLayout;
71     StateSet mStateSet = null;
72     Transition mCurrentTransition = null;
73     private boolean mDisableAutoTransition = false;
74     private ArrayList<Transition> mTransitionList = new ArrayList<>();
75     private Transition mDefaultTransition = null;
76     private ArrayList<Transition> mAbstractTransitionList = new ArrayList<>();
77 
78     private SparseArray<ConstraintSet> mConstraintSetMap = new SparseArray<>();
79     private HashMap<String, Integer> mConstraintSetIdMap = new HashMap<>();
80     private SparseIntArray mDeriveMap = new SparseIntArray();
81     private static final boolean DEBUG_DESKTOP = false;
82     private int mDefaultDuration = 400;
83     private int mLayoutDuringTransition = 0;
84     public static final int LAYOUT_IGNORE_REQUEST = 0;
85     public static final int LAYOUT_HONOR_REQUEST = 1;
86     public static final int LAYOUT_CALL_MEASURE = 2;
87 
88     private MotionEvent mLastTouchDown;
89     private boolean mIgnoreTouch = false;
90     private boolean mMotionOutsideRegion = false;
91     private MotionLayout.MotionTracker mVelocityTracker; // used to support fling
92     private boolean mRtl;
93     private static final String MOTIONSCENE_TAG = "MotionScene";
94     private static final String TRANSITION_TAG = "Transition";
95     private static final String ONSWIPE_TAG = "OnSwipe";
96     private static final String ONCLICK_TAG = "OnClick";
97     private static final String STATESET_TAG = "StateSet";
98     private static final String INCLUDE_TAG_UC = "Include";
99     private static final String INCLUDE_TAG = "include";
100     private static final String KEYFRAMESET_TAG = "KeyFrameSet";
101     private static final String CONSTRAINTSET_TAG = "ConstraintSet";
102     private static final String VIEW_TRANSITION = "ViewTransition";
103     final ViewTransitionController mViewTransitionController;
104 
105     /**
106      * Set the transition between two constraint set / states.
107      * The transition will get created between the two sets
108      * if it doesn't exist already.
109      *
110      * @param beginId id of the start constraint set or state
111      * @param endId   id of the end constraint set or state
112      */
setTransition(int beginId, int endId)113     void setTransition(int beginId, int endId) {
114         int start = beginId;
115         int end = endId;
116         if (mStateSet != null) {
117             int tmp = mStateSet.stateGetConstraintID(beginId, -1, -1);
118             if (tmp != -1) {
119                 start = tmp;
120             }
121             tmp = mStateSet.stateGetConstraintID(endId, -1, -1);
122             if (tmp != -1) {
123                 end = tmp;
124             }
125         }
126         if (DEBUG) {
127             Log.v(TAG, Debug.getLocation() + " setTransition "
128                     + Debug.getName(mMotionLayout.getContext(), beginId) + " -> "
129                     + Debug.getName(mMotionLayout.getContext(), endId));
130         }
131         if (mCurrentTransition != null) {
132             if (mCurrentTransition.mConstraintSetEnd == endId
133                     && mCurrentTransition.mConstraintSetStart == beginId) {
134                 return;
135             }
136         }
137         for (Transition transition : mTransitionList) {
138             if ((transition.mConstraintSetEnd == end
139                     && transition.mConstraintSetStart == start)
140                     || (transition.mConstraintSetEnd == endId
141                     && transition.mConstraintSetStart == beginId)) {
142                 if (DEBUG) {
143                     Log.v(TAG, Debug.getLocation() + " found transition  "
144                             + Debug.getName(mMotionLayout.getContext(), beginId) + " -> "
145                             + Debug.getName(mMotionLayout.getContext(), endId));
146                 }
147                 mCurrentTransition = transition;
148                 if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
149                     mCurrentTransition.mTouchResponse.setRTL(mRtl);
150                 }
151                 return;
152             }
153         }
154         // No transition defined for this so we will create one?
155         Transition matchTransition = mDefaultTransition;
156         for (Transition transition : mAbstractTransitionList) {
157             if (transition.mConstraintSetEnd == endId) {
158                 matchTransition = transition;
159             }
160         }
161         Transition t = new Transition(this, matchTransition);
162 
163         t.mConstraintSetStart = start;
164         t.mConstraintSetEnd = end;
165         if (start != UNSET) {
166             mTransitionList.add(t);
167         }
168         mCurrentTransition = t;
169     }
170 
171     /**
172      * Add a transition to the motion scene. If a transition with the same id already exists
173      * in the scene, the new transition will replace the existing one.
174      *
175      * @throws IllegalArgumentException if the transition does not have an id.
176      */
addTransition(Transition transition)177     public void addTransition(Transition transition) {
178         int index = getIndex(transition);
179         if (index == -1) {
180             mTransitionList.add(transition);
181         } else {
182             mTransitionList.set(index, transition);
183         }
184     }
185 
186     /**
187      * Remove the transition with the matching id from the motion scene. If no matching transition
188      * is found, it does nothing.
189      *
190      * @throws IllegalArgumentException if the transition does not have an id.
191      */
removeTransition(Transition transition)192     public void removeTransition(Transition transition) {
193         int index = getIndex(transition);
194         if (index != -1) {
195             mTransitionList.remove(index);
196         }
197     }
198 
199     /**
200      * @return the index in the transition list. -1 if transition wasn't found.
201      */
getIndex(Transition transition)202     private int getIndex(Transition transition) {
203         int id = transition.mId;
204         if (id == UNSET) {
205             throw new IllegalArgumentException("The transition must have an id");
206         }
207 
208         int index = 0;
209         for (; index < mTransitionList.size(); index++) {
210             if (mTransitionList.get(index).mId == id) {
211                 return index;
212             }
213         }
214 
215         return -1;
216     }
217 
218     /**
219      * @return true if the layout is valid for the scene. False otherwise. Use it for the debugging
220      * purposes.
221      */
validateLayout(MotionLayout layout)222     public boolean validateLayout(MotionLayout layout) {
223         return (layout == mMotionLayout && layout.mScene == this);
224     }
225 
226     /**
227      * Set the transition to be the current transition of the motion scene.
228      *
229      * @param transition a transition to be set. The transition must exist within the motion scene.
230      *                   (e.g. {@link #addTransition(Transition)})
231      */
setTransition(Transition transition)232     public void setTransition(Transition transition) {
233         mCurrentTransition = transition;
234         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
235             mCurrentTransition.mTouchResponse.setRTL(mRtl);
236         }
237     }
238 
getRealID(int stateId)239     private int getRealID(int stateId) {
240         if (mStateSet != null) {
241             int tmp = mStateSet.stateGetConstraintID(stateId, -1, -1);
242             if (tmp != -1) {
243                 return tmp;
244             }
245         }
246         return stateId;
247     }
248 
249     /**
250      * Get all transitions that include this state
251      * @param stateId
252      * @return
253      */
getTransitionsWithState(int stateId)254     public List<Transition> getTransitionsWithState(int stateId) {
255         stateId = getRealID(stateId);
256         ArrayList<Transition> ret = new ArrayList<>();
257         for (Transition transition : mTransitionList) {
258             if (transition.mConstraintSetStart == stateId
259                     || transition.mConstraintSetEnd == stateId) {
260                 ret.add(transition);
261             }
262 
263         }
264         return ret;
265     }
266 
267     /**
268      * Add all on click listeners for the current state
269      * @param motionLayout
270      * @param currentState
271      */
addOnClickListeners(MotionLayout motionLayout, int currentState)272     public void addOnClickListeners(MotionLayout motionLayout, int currentState) {
273         // remove all on clicks listeners
274         for (Transition transition : mTransitionList) {
275             if (transition.mOnClicks.size() > 0) {
276                 for (Transition.TransitionOnClick onClick : transition.mOnClicks) {
277                     onClick.removeOnClickListeners(motionLayout);
278                 }
279             }
280         }
281         for (Transition transition : mAbstractTransitionList) {
282             if (transition.mOnClicks.size() > 0) {
283                 for (Transition.TransitionOnClick onClick : transition.mOnClicks) {
284                     onClick.removeOnClickListeners(motionLayout);
285                 }
286             }
287         }
288         // add back all the listeners that are needed
289         for (Transition transition : mTransitionList) {
290             if (transition.mOnClicks.size() > 0) {
291                 for (Transition.TransitionOnClick onClick : transition.mOnClicks) {
292                     onClick.addOnClickListeners(motionLayout, currentState, transition);
293                 }
294             }
295         }
296         for (Transition transition : mAbstractTransitionList) {
297             if (transition.mOnClicks.size() > 0) {
298                 for (Transition.TransitionOnClick onClick : transition.mOnClicks) {
299                     onClick.addOnClickListeners(motionLayout, currentState, transition);
300                 }
301             }
302         }
303     }
304 
305     /**
306      * Find the best transition for the motion
307      * @param currentState
308      * @param dx drag delta x
309      * @param dy drag delta y
310      * @param lastTouchDown
311      * @return
312      */
bestTransitionFor(int currentState, float dx, float dy, MotionEvent lastTouchDown)313     public Transition bestTransitionFor(int currentState,
314                                         float dx,
315                                         float dy,
316                                         MotionEvent lastTouchDown) {
317         List<Transition> candidates = null;
318         if (currentState != -1) {
319             candidates = getTransitionsWithState(currentState);
320             float max = 0;
321             Transition best = null;
322             RectF cache = new RectF();
323             for (Transition transition : candidates) {
324                 if (transition.mDisable) {
325                     continue;
326                 }
327                 if (transition.mTouchResponse != null) {
328                     transition.mTouchResponse.setRTL(mRtl);
329                     RectF region = transition.mTouchResponse.getTouchRegion(mMotionLayout, cache);
330                     if (region != null && lastTouchDown != null
331                             && !region.contains(lastTouchDown.getX(), lastTouchDown.getY())) {
332                         continue;
333                     }
334                     region = transition.mTouchResponse.getLimitBoundsTo(mMotionLayout, cache);
335                     if (region != null && lastTouchDown != null
336                             && !region.contains(lastTouchDown.getX(), lastTouchDown.getY())) {
337                         continue;
338                     }
339 
340                     float val = transition.mTouchResponse.dot(dx, dy);
341                     if (transition.mTouchResponse.mIsRotateMode && lastTouchDown != null) {
342                         float startX = lastTouchDown.getX()
343                                 - transition.mTouchResponse.mRotateCenterX;
344                         float startY = lastTouchDown.getY()
345                                 - transition.mTouchResponse.mRotateCenterY;
346                         float endX = dx + startX;
347                         float endY = dy + startY;
348                         double endAngle = Math.atan2(endY, endX);
349                         double startAngle = Math.atan2(startX, startY);
350                         val = (float) (endAngle - startAngle) * 10;
351                     }
352                     if (transition.mConstraintSetEnd == currentState) { // flip because backwards
353                         val *= -1;
354                     } else {
355                         val *= 1.1f; // slightly bias towards the transition which is start over end
356                     }
357 
358                     if (val > max) {
359                         max = val;
360                         best = transition;
361                     }
362                 }
363             }
364             if (DEBUG) {
365                 if (best != null) {
366                     Log.v(TAG, Debug.getLocation() + "  ### BEST ----- "
367                             + best.debugString(mMotionLayout.getContext()) + " ----");
368                 } else {
369                     Log.v(TAG, Debug.getLocation() + "  ### BEST ----- " + null + " ----");
370 
371                 }
372             }
373             return best;
374         }
375         return mCurrentTransition;
376     }
377 
378     /**
379      * Get list of Transitions know to the system
380      * @return
381      */
getDefinedTransitions()382     public ArrayList<Transition> getDefinedTransitions() {
383         return mTransitionList;
384     }
385 
386     /**
387      * Find the transition based on the id
388      * @param id
389      * @return
390      */
getTransitionById(int id)391     public Transition getTransitionById(int id) {
392         for (Transition transition : mTransitionList) {
393             if (transition.mId == id) {
394                 return transition;
395             }
396         }
397         return null;
398     }
399 
400     /**
401      * Get the list of all Constraint Sets Know to the system
402      * @return
403      */
getConstraintSetIds()404     public int[] getConstraintSetIds() {
405         int[] ids = new int[mConstraintSetMap.size()];
406         for (int i = 0; i < ids.length; i++) {
407             ids[i] = mConstraintSetMap.keyAt(i);
408         }
409         return ids;
410     }
411 
412     /**
413      * Get the id's of all constraintSets with the matching types
414      * @return
415      */
getMatchingStateLabels(String .... types)416     public int[] getMatchingStateLabels(String ... types) {
417         int[] ids = new int[mConstraintSetMap.size()];
418 
419         int count = 0;
420 
421         for (int i = 0; i < ids.length; i++) {
422             ConstraintSet set = mConstraintSetMap.valueAt(i);
423             int id = mConstraintSetMap.keyAt(i);
424             if (set.matchesLabels(types)) {
425                 @SuppressWarnings("unused") String[] s = set.getStateLabels();
426                 ids[count++] = id;
427             }
428         }
429         return Arrays.copyOf(ids, count);
430     }
431 
432     /**
433      * This will launch a transition to another state if an autoTransition is enabled on
434      * a Transition that matches the current state.
435      *
436      * @param motionLayout
437      * @param currentState
438      * @return
439      *
440      */
autoTransition(MotionLayout motionLayout, int currentState)441     boolean autoTransition(MotionLayout motionLayout, int currentState) {
442         if (isProcessingTouch()) {
443             return false;
444         }
445         if (mDisableAutoTransition) {
446             return false;
447         }
448 
449         for (Transition transition : mTransitionList) {
450             if (transition.mAutoTransition == Transition.AUTO_NONE) {
451                 continue;
452             }
453             if (mCurrentTransition == transition
454                     && mCurrentTransition.isTransitionFlag(Transition.TRANSITION_FLAG_INTRA_AUTO)) {
455                 continue;
456             }
457             if (currentState == transition.mConstraintSetStart && (
458                     transition.mAutoTransition == Transition.AUTO_ANIMATE_TO_END
459                             || transition.mAutoTransition == Transition.AUTO_JUMP_TO_END)) {
460                 motionLayout.setState(MotionLayout.TransitionState.FINISHED);
461                 motionLayout.setTransition(transition);
462                 if (transition.mAutoTransition == Transition.AUTO_ANIMATE_TO_END) {
463                     motionLayout.transitionToEnd();
464                     motionLayout.setState(MotionLayout.TransitionState.SETUP);
465                     motionLayout.setState(MotionLayout.TransitionState.MOVING);
466                 } else {
467                     motionLayout.setProgress(1);
468                     motionLayout.evaluate(true);
469                     motionLayout.setState(MotionLayout.TransitionState.SETUP);
470                     motionLayout.setState(MotionLayout.TransitionState.MOVING);
471                     motionLayout.setState(MotionLayout.TransitionState.FINISHED);
472                     motionLayout.onNewStateAttachHandlers();
473                 }
474                 return true;
475             }
476             if (currentState == transition.mConstraintSetEnd && (
477                     transition.mAutoTransition == Transition.AUTO_ANIMATE_TO_START
478                             || transition.mAutoTransition == Transition.AUTO_JUMP_TO_START)) {
479                 motionLayout.setState(MotionLayout.TransitionState.FINISHED);
480                 motionLayout.setTransition(transition);
481                 if (transition.mAutoTransition == Transition.AUTO_ANIMATE_TO_START) {
482                     motionLayout.transitionToStart();
483                     motionLayout.setState(MotionLayout.TransitionState.SETUP);
484                     motionLayout.setState(MotionLayout.TransitionState.MOVING);
485                 } else {
486                     motionLayout.setProgress(0);
487                     motionLayout.evaluate(true);
488                     motionLayout.setState(MotionLayout.TransitionState.SETUP);
489                     motionLayout.setState(MotionLayout.TransitionState.MOVING);
490                     motionLayout.setState(MotionLayout.TransitionState.FINISHED);
491                     motionLayout.onNewStateAttachHandlers();
492                 }
493                 return true;
494             }
495         }
496         return false;
497     }
498 
isProcessingTouch()499     private boolean isProcessingTouch() {
500         return (mVelocityTracker != null);
501     }
502 
503     /**
504      * Set Right to left
505      * @param rtl
506      */
setRtl(boolean rtl)507     public void setRtl(boolean rtl) {
508         mRtl = rtl;
509         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
510             mCurrentTransition.mTouchResponse.setRTL(mRtl);
511         }
512     }
513 
514     /**
515      * Apply the viewTransition on the list of views
516      * @param id
517      * @param view
518      */
viewTransition(int id, View... view)519     public void viewTransition(int id, View... view) {
520         mViewTransitionController.viewTransition(id, view);
521     }
522 
523     /**
524      * Enable this viewTransition
525      * @param id of viewTransition
526      * @param enable
527      */
enableViewTransition(int id, boolean enable)528     public void enableViewTransition(int id, boolean enable) {
529         mViewTransitionController.enableViewTransition(id, enable);
530     }
531 
532     /**
533      * Is this view transition enabled
534      * @param id of viewTransition
535      * @return
536      */
isViewTransitionEnabled(int id)537     public boolean isViewTransitionEnabled(int id) {
538         return mViewTransitionController.isViewTransitionEnabled(id);
539     }
540 
541     /**
542      * Apply a view transition to the MotionController
543      * @param viewTransitionId of viewTransition
544      * @param motionController
545      * @return
546      */
applyViewTransition(int viewTransitionId, MotionController motionController)547     public boolean applyViewTransition(int viewTransitionId, MotionController motionController) {
548         return mViewTransitionController.applyViewTransition(viewTransitionId, motionController);
549     }
550 
551 ///////////////////////////////////////////////////////////////////////////////
552 // ====================== Transition ==========================================
553 
554     /**
555      * Transition defines the interaction from one state to another.
556      * With out a Transition object Transition between two stats involves strictly linear
557      * interpolation
558      */
559     public static class Transition {
560         private int mId = UNSET;
561         private boolean mIsAbstract = false;
562         private int mConstraintSetEnd = -1;
563         private int mConstraintSetStart = -1;
564         private int mDefaultInterpolator = 0;
565         private String mDefaultInterpolatorString = null;
566         private int mDefaultInterpolatorID = -1;
567         private int mDuration = 400;
568         private float mStagger = 0.0f;
569         private final MotionScene mMotionScene;
570         private ArrayList<KeyFrames> mKeyFramesList = new ArrayList<>();
571         private TouchResponse mTouchResponse = null;
572         private ArrayList<TransitionOnClick> mOnClicks = new ArrayList<>();
573         private int mAutoTransition = 0;
574         public static final int AUTO_NONE = 0;
575         public static final int AUTO_JUMP_TO_START = 1;
576         public static final int AUTO_JUMP_TO_END = 2;
577         public static final int AUTO_ANIMATE_TO_START = 3;
578         public static final int AUTO_ANIMATE_TO_END = 4;
579         private boolean mDisable = false;
580         private int mPathMotionArc = UNSET;
581         private int mLayoutDuringTransition = 0;
582         private int mTransitionFlags = 0;
583         static final int TRANSITION_FLAG_FIRST_DRAW = 1;
584         static final int TRANSITION_FLAG_INTRA_AUTO = 2;
585         static final int TRANSITION_FLAG_INTERCEPT_TOUCH = 4;
586 
587         public static final int INTERPOLATE_REFERENCE_ID = -2;
588         public static final int INTERPOLATE_SPLINE_STRING = -1;
589         public static final int INTERPOLATE_EASE_IN_OUT = 0;
590         public static final int INTERPOLATE_EASE_IN = 1;
591         public static final int INTERPOLATE_EASE_OUT = 2;
592         public static final int INTERPOLATE_LINEAR = 3;
593         public static final int INTERPOLATE_BOUNCE = 4;
594         public static final int INTERPOLATE_OVERSHOOT = 5;
595         public static final int INTERPOLATE_ANTICIPATE = 6;
596 
597         /**
598          * Set the onSwipe for this Transition
599          * @param onSwipe
600          */
setOnSwipe(OnSwipe onSwipe)601         public void setOnSwipe(OnSwipe onSwipe) {
602             mTouchResponse = (onSwipe == null) ? null
603                     : new TouchResponse(mMotionScene.mMotionLayout, onSwipe);
604         }
605 
606         /**
607          * Add the onclick to this view
608          * @param id
609          * @param action
610          */
addOnClick(int id, int action)611         public void addOnClick(int id, int action) {
612             for (TransitionOnClick onClick : mOnClicks) {
613                 if (onClick.mTargetId == id) {
614                     onClick.mMode = action;
615                     return;
616                 }
617             }
618             TransitionOnClick click = new TransitionOnClick(this, id, action);
619             mOnClicks.add(click);
620         }
621 
622         /**
623          * Remove the onclick added to this view
624          * @param id
625          */
removeOnClick(int id)626         public void removeOnClick(int id) {
627             TransitionOnClick toRemove = null;
628             for (TransitionOnClick onClick : mOnClicks) {
629                 if (onClick.mTargetId == id) {
630                     toRemove = onClick;
631                     break;
632                 }
633             }
634             if (toRemove != null) {
635                 mOnClicks.remove(toRemove);
636             }
637         }
638 
639         /**
640          * get the mode of layout during transition
641          * @return
642          */
getLayoutDuringTransition()643         public int getLayoutDuringTransition() {
644             return mLayoutDuringTransition;
645         }
646 
647         /**
648          * set the mode of layout during transition
649          * @param mode
650          */
setLayoutDuringTransition(int mode)651         public void setLayoutDuringTransition(int mode) {
652             mLayoutDuringTransition = mode;
653         }
654 
655         /**
656          * Add on Click support using the xml parser
657          * @param context
658          * @param parser
659          */
addOnClick(Context context, XmlPullParser parser)660         public void addOnClick(Context context, XmlPullParser parser) {
661             mOnClicks.add(new TransitionOnClick(context, this, parser));
662         }
663 
664         /**
665          * sets the autoTransitionType
666          * On reaching a state auto transitions may be run based on
667          * one of AUTO_NONE, AUTO_JUMP_TO_START, AUTO_JUMP_TO_END, AUTO_ANIMATE_TO_START,
668          * AUTO_ANIMATE_TO_END
669          *
670          * @param type
671          */
setAutoTransition(int type)672         public void setAutoTransition(int type) {
673             mAutoTransition = type;
674         }
675 
676         /**
677          * return the autoTransitionType.
678          * one of AUTO_NONE, AUTO_JUMP_TO_START, AUTO_JUMP_TO_END, AUTO_ANIMATE_TO_START,
679          * AUTO_ANIMATE_TO_END
680          *
681          * @return 0=NONE, 1=JUMP_TO_START, 2=JUMP_TO_END, 3=ANIMATE_TO_START, 4=ANIMATE_TO_END
682          */
getAutoTransition()683         public int getAutoTransition() {
684             return mAutoTransition;
685         }
686 
687         /**
688          * Transitions can be given and ID. If unset it returns UNSET (-1)
689          *
690          * @return The Id of the Transition set in the MotionScene File or UNSET (-1)
691          */
getId()692         public int getId() {
693             return mId;
694         }
695 
696         /**
697          * Get the id of the constraint set to go to
698          *
699          * @return
700          */
getEndConstraintSetId()701         public int getEndConstraintSetId() {
702             return mConstraintSetEnd;
703         }
704 
705         /**
706          * Gets the id of the starting constraint set
707          *
708          * @return
709          */
getStartConstraintSetId()710         public int getStartConstraintSetId() {
711             return mConstraintSetStart;
712         }
713 
714         /**
715          * sets the duration of the transition
716          * if set to < 8 it will be set to 8
717          *
718          * @param duration in milliseconds (min is 8)
719          */
setDuration(int duration)720         public void setDuration(int duration) {
721             this.mDuration = Math.max(duration, MIN_DURATION);
722         }
723 
724         /**
725          * gets the default transition duration
726          *
727          * @return duration int milliseconds
728          */
getDuration()729         public int getDuration() {
730             return mDuration;
731         }
732 
733         /**
734          * Gets the stagger value.
735          *
736          * @return
737          */
getStagger()738         public float getStagger() {
739             return mStagger;
740         }
741 
getKeyFrameList()742         public List<KeyFrames> getKeyFrameList() {
743             return mKeyFramesList;
744         }
745 
746         /**
747          *  add a keyframe to this motion scene
748          * @param keyFrames
749          */
addKeyFrame(KeyFrames keyFrames)750         public void addKeyFrame(KeyFrames keyFrames) {
751             mKeyFramesList.add(keyFrames);
752         }
753 
754         /**
755          * Get the onClick handlers.
756          *
757          * @return list of on click handler
758          */
getOnClickList()759         public List<TransitionOnClick> getOnClickList() {
760             return mOnClicks;
761         }
762 
763         /**
764          * Get the Touch response manager
765          *
766          * @return
767          */
768         @SuppressWarnings("HiddenTypeParameter")
getTouchResponse()769         public TouchResponse getTouchResponse() {
770             return mTouchResponse;
771         }
772 
773         /**
774          * Sets the stagger value.
775          * A Stagger value of zero means no stagger.
776          * A Stagger value of 1 means the last view starts moving at .5 progress
777          *
778          * @param stagger
779          */
setStagger(float stagger)780         public void setStagger(float stagger) {
781             mStagger = stagger;
782         }
783 
784         /**
785          * Sets the pathMotionArc for the all motions in this transition.
786          * if set to UNSET (default) it reverts to the setting of the constraintSet
787          *
788          * @param arcMode
789          */
setPathMotionArc(int arcMode)790         public void setPathMotionArc(int arcMode) {
791             mPathMotionArc = arcMode;
792         }
793 
794         /**
795          * gets the pathMotionArc for the all motions in this transition.
796          * if set to UNSET (default) it reverts to the setting of the constraintSet
797          *
798          * @return arcMode
799          */
getPathMotionArc()800         public int getPathMotionArc() {
801             return mPathMotionArc;
802         }
803 
804         /**
805          * Returns true if this Transition can be auto considered for transition
806          * Default is enabled
807          */
isEnabled()808         public boolean isEnabled() {
809             return !mDisable;
810         }
811 
812         /**
813          * enable or disable the Transition. If a Transition is disabled it is not eligible
814          * for automatically switching to.
815          *
816          * @param enable
817          */
setEnabled(boolean enable)818         public void setEnabled(boolean enable) {
819             mDisable = !enable;
820         }
821 
822         /**
823          * Print a debug string indicating the starting and ending state of the transition
824          *
825          * @param context
826          * @return
827          */
debugString(Context context)828         public String debugString(Context context) {
829 
830             String ret;
831             if (mConstraintSetStart == UNSET) {
832                 ret = "null";
833             } else {
834                 ret = context.getResources().getResourceEntryName(mConstraintSetStart);
835             }
836             if (mConstraintSetEnd == UNSET) {
837                 ret += " -> " + "null";
838             } else {
839                 ret += " -> " + context.getResources().getResourceEntryName(mConstraintSetEnd);
840             }
841             return ret;
842         }
843 
844         /**
845          * is the transition flag set
846          * @param flag
847          * @return
848          */
isTransitionFlag(int flag)849         public boolean isTransitionFlag(int flag) {
850             return 0 != (mTransitionFlags & flag);
851         }
852 
setTransitionFlag(int flag)853         public void setTransitionFlag(int flag) {
854             mTransitionFlags = flag;
855         }
856 
857         /**
858          * Set the on touch up mode
859          * @param touchUpMode
860          */
setOnTouchUp(int touchUpMode)861         public void setOnTouchUp(int touchUpMode) {
862             TouchResponse touchResponse = getTouchResponse();
863             if (touchResponse != null) {
864                 touchResponse.setTouchUpMode(touchUpMode);
865             }
866         }
867 
868         public static class TransitionOnClick implements View.OnClickListener {
869             public static final int ANIM_TO_END = 0x0001;
870             public static final int ANIM_TOGGLE = 0x0011;
871             public static final int ANIM_TO_START = 0x0010;
872             public static final int JUMP_TO_END = 0x100;
873             public static final int JUMP_TO_START = 0x1000;
874             private final Transition mTransition;
875             int mTargetId = UNSET;
876             int mMode = 0x11;
877 
TransitionOnClick(Context context, Transition transition, XmlPullParser parser)878             public TransitionOnClick(Context context,
879                                      Transition transition,
880                                      XmlPullParser parser) {
881                 mTransition = transition;
882                 TypedArray a = context.obtainStyledAttributes(Xml.asAttributeSet(parser),
883                         R.styleable.OnClick);
884                 final int count = a.getIndexCount();
885                 for (int i = 0; i < count; i++) {
886                     int attr = a.getIndex(i);
887                     if (attr == R.styleable.OnClick_targetId) {
888                         mTargetId = a.getResourceId(attr, mTargetId);
889                     } else if (attr == R.styleable.OnClick_clickAction) {
890                         mMode = a.getInt(attr, mMode);
891                     }
892                 }
893                 a.recycle();
894             }
895 
TransitionOnClick(Transition transition, int id, int action)896             public TransitionOnClick(Transition transition, int id, int action) {
897                 mTransition = transition;
898                 mTargetId = id;
899                 mMode = action;
900             }
901 
902             /**
903              * Add the on click listeners for the current state
904              *
905              * @param motionLayout
906              * @param currentState
907              * @param transition
908              */
addOnClickListeners(MotionLayout motionLayout, int currentState, Transition transition)909             public void addOnClickListeners(MotionLayout motionLayout,
910                                             int currentState,
911                                             Transition transition) {
912                 View v = mTargetId == UNSET ? motionLayout : motionLayout.findViewById(mTargetId);
913                 if (v == null) {
914                     Log.e(TAG, "OnClick could not find id " + mTargetId);
915                     return;
916                 }
917                 int start = transition.mConstraintSetStart;
918                 int end = transition.mConstraintSetEnd;
919                 if (start == UNSET) { // does not require a known end state
920                     v.setOnClickListener(this);
921                     return;
922                 }
923 
924                 boolean listen = ((mMode & ANIM_TO_END) != 0) && currentState == start;
925                 listen |= ((mMode & JUMP_TO_END) != 0) && currentState == start;
926                 listen |= ((mMode & ANIM_TO_END) != 0) && currentState == start;
927                 listen |= ((mMode & ANIM_TO_START) != 0) && currentState == end;
928                 listen |= ((mMode & JUMP_TO_START) != 0) && currentState == end;
929 
930                 if (listen) {
931                     v.setOnClickListener(this);
932                 }
933             }
934 
935             /**
936              * Remove the OnClickListeners
937              * (typically called because you are removing the transition)
938              *
939              * @param motionLayout
940              */
removeOnClickListeners(MotionLayout motionLayout)941             public void removeOnClickListeners(MotionLayout motionLayout) {
942                 if (mTargetId == UNSET) {
943                     return;
944                 }
945                 View v = motionLayout.findViewById(mTargetId);
946                 if (v == null) {
947                     Log.e(TAG, " (*)  could not find id " + mTargetId);
948                     return;
949                 }
950                 v.setOnClickListener(null);
951             }
952 
isTransitionViable(Transition current, MotionLayout tl)953             boolean isTransitionViable(Transition current, MotionLayout tl) {
954                 if (mTransition == current) {
955                     return true;
956                 }
957                 int dest = mTransition.mConstraintSetEnd;
958                 int from = mTransition.mConstraintSetStart;
959                 if (from == UNSET) {
960                     return tl.mCurrentState != dest;
961                 }
962                 return (tl.mCurrentState == from) || (tl.mCurrentState == dest);
963 
964             }
965 
966             @Override
onClick(View view)967             public void onClick(View view) {
968                 MotionLayout tl = mTransition.mMotionScene.mMotionLayout;
969                 if (!tl.isInteractionEnabled()) {
970                     return;
971                 }
972                 if (mTransition.mConstraintSetStart == UNSET) {
973                     int currentState = tl.getCurrentState();
974                     if (currentState == UNSET) {
975                         tl.transitionToState(mTransition.mConstraintSetEnd);
976                         return;
977                     }
978                     Transition t = new Transition(mTransition.mMotionScene, mTransition);
979                     t.mConstraintSetStart = currentState;
980                     t.mConstraintSetEnd = mTransition.mConstraintSetEnd;
981                     tl.setTransition(t);
982                     tl.transitionToEnd();
983                     return;
984                 }
985                 Transition current = mTransition.mMotionScene.mCurrentTransition;
986                 boolean forward = ((mMode & ANIM_TO_END) != 0 || (mMode & JUMP_TO_END) != 0);
987                 boolean backward = ((mMode & ANIM_TO_START) != 0 || (mMode & JUMP_TO_START) != 0);
988                 boolean bidirectional = forward && backward;
989                 if (bidirectional) {
990                     if (mTransition.mMotionScene.mCurrentTransition != mTransition) {
991                         tl.setTransition(mTransition);
992                     }
993                     if (tl.getCurrentState() == tl.getEndState() || tl.getProgress() > 0.5f) {
994                         forward = false;
995                     } else {
996                         backward = false;
997                     }
998                 }
999                 if (isTransitionViable(current, tl)) {
1000                     if (forward && (mMode & ANIM_TO_END) != 0) {
1001                         tl.setTransition(mTransition);
1002                         tl.transitionToEnd();
1003                     } else if (backward && (mMode & ANIM_TO_START) != 0) {
1004                         tl.setTransition(mTransition);
1005                         tl.transitionToStart();
1006                     } else if (forward && (mMode & JUMP_TO_END) != 0) {
1007                         tl.setTransition(mTransition);
1008                         tl.setProgress(1);
1009                     } else if (backward && (mMode & JUMP_TO_START) != 0) {
1010                         tl.setTransition(mTransition);
1011                         tl.setProgress(0);
1012                     }
1013                 }
1014             }
1015         }
1016 
Transition(MotionScene motionScene, Transition global)1017         Transition(MotionScene motionScene, Transition global) {
1018             mMotionScene = motionScene;
1019             mDuration = motionScene.mDefaultDuration;
1020             if (global != null) {
1021                 mPathMotionArc = global.mPathMotionArc;
1022                 mDefaultInterpolator = global.mDefaultInterpolator;
1023                 mDefaultInterpolatorString = global.mDefaultInterpolatorString;
1024                 mDefaultInterpolatorID = global.mDefaultInterpolatorID;
1025                 mDuration = global.mDuration;
1026                 mKeyFramesList = global.mKeyFramesList;
1027                 mStagger = global.mStagger;
1028                 mLayoutDuringTransition = global.mLayoutDuringTransition;
1029             }
1030         }
1031 
1032         /**
1033          * Create a transition
1034          *
1035          * @param id                   a unique id to represent the transition.
1036          * @param motionScene          the motion scene that the transition will be added to.
1037          * @param constraintSetStartId id of the ConstraintSet to be used for the start of
1038          *                             transition
1039          * @param constraintSetEndId   id of the ConstraintSet to be used for the end of transition
1040          */
Transition( int id, MotionScene motionScene, int constraintSetStartId, int constraintSetEndId)1041         public Transition(
1042                 int id,
1043                 MotionScene motionScene,
1044                 int constraintSetStartId,
1045                 int constraintSetEndId) {
1046             mId = id;
1047             mMotionScene = motionScene;
1048             mConstraintSetStart = constraintSetStartId;
1049             mConstraintSetEnd = constraintSetEndId;
1050             mDuration = motionScene.mDefaultDuration;
1051             mLayoutDuringTransition = motionScene.mLayoutDuringTransition;
1052         }
1053 
Transition(MotionScene motionScene, Context context, XmlPullParser parser)1054         Transition(MotionScene motionScene, Context context, XmlPullParser parser) {
1055             mDuration = motionScene.mDefaultDuration;
1056             mLayoutDuringTransition = motionScene.mLayoutDuringTransition;
1057             mMotionScene = motionScene;
1058             fillFromAttributeList(motionScene, context, Xml.asAttributeSet(parser));
1059         }
1060 
1061         /**
1062          * Sets the interpolation used for this transition.
1063          * <br>
1064          * The call support standard types EASE_IN_OUT etc.:<br>
1065          * setInterpolatorInfo(MotionScene.Transition.INTERPOLATE_EASE_IN_OUT, null, 0);
1066          * setInterpolatorInfo(MotionScene.Transition.INTERPOLATE_OVERSHOOT, null, 0);
1067          * <br>
1068          * Strings such as "cubic(...)" , "spline(...)"<br>
1069          * setInterpolatorInfo(
1070          * MotionScene.Transition.INTERPOLATE_SPLINE_STRING, "cubic(1,0,0,1)", 0);
1071          * <br>
1072          * Android interpolators in res/anim : <br>
1073          * setInterpolatorInfo(
1074          * MotionScene.Transition.INTERPOLATE_REFERENCE_ID, null, R.anim....);
1075          * <br>
1076          * @param interpolator sets the type of interpolation (MotionScene.Transition.INTERPOLATE_*)
1077          * @param interpolatorString sets a string based custom interpolation
1078          * @param interpolatorID sets the id of a Android Transition
1079          */
setInterpolatorInfo(int interpolator, String interpolatorString, int interpolatorID)1080         public void setInterpolatorInfo(int interpolator,
1081                                         String interpolatorString,
1082                                         int interpolatorID) {
1083             mDefaultInterpolator = interpolator;
1084             mDefaultInterpolatorString = interpolatorString;
1085             mDefaultInterpolatorID = interpolatorID;
1086         }
1087 
fillFromAttributeList(MotionScene motionScene, Context context, AttributeSet attrs)1088         private void fillFromAttributeList(MotionScene motionScene,
1089                                            Context context,
1090                                            AttributeSet attrs) {
1091             TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Transition);
1092             fill(motionScene, context, a);
1093             a.recycle();
1094         }
1095 
fill(MotionScene motionScene, Context context, TypedArray a)1096         private void fill(MotionScene motionScene, Context context, TypedArray a) {
1097             final int count = a.getIndexCount();
1098             for (int i = 0; i < count; i++) {
1099                 int attr = a.getIndex(i);
1100                 if (attr == R.styleable.Transition_constraintSetEnd) {
1101                     mConstraintSetEnd = a.getResourceId(attr, UNSET);
1102                     String type = context.getResources().getResourceTypeName(mConstraintSetEnd);
1103                     if ("layout".equals(type)) {
1104                         ConstraintSet cSet = new ConstraintSet();
1105                         cSet.load(context, mConstraintSetEnd);
1106                         motionScene.mConstraintSetMap.append(mConstraintSetEnd, cSet);
1107                         if (DEBUG) {
1108                             Log.v(TAG, " constraint Set end loaded from layout "
1109                                     + Debug.getName(context, mConstraintSetEnd));
1110                         }
1111                     } else if ("xml".equals(type)) {
1112                         int id = motionScene.parseInclude(context, mConstraintSetEnd);
1113                         mConstraintSetEnd = id;
1114                     }
1115                 } else if (attr == R.styleable.Transition_constraintSetStart) {
1116                     mConstraintSetStart = a.getResourceId(attr, mConstraintSetStart);
1117                     String type = context.getResources().getResourceTypeName(mConstraintSetStart);
1118                     if ("layout".equals(type)) {
1119                         ConstraintSet cSet = new ConstraintSet();
1120                         cSet.load(context, mConstraintSetStart);
1121                         motionScene.mConstraintSetMap.append(mConstraintSetStart, cSet);
1122                     } else if ("xml".equals(type)) {
1123                         int id = motionScene.parseInclude(context, mConstraintSetStart);
1124                         mConstraintSetStart = id;
1125                     }
1126                 } else if (attr == R.styleable.Transition_motionInterpolator) {
1127                     TypedValue type = a.peekValue(attr);
1128 
1129                     if (type.type == TypedValue.TYPE_REFERENCE) {
1130                         mDefaultInterpolatorID = a.getResourceId(attr, -1);
1131                         if (mDefaultInterpolatorID != -1) {
1132                             mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID;
1133                         }
1134                     } else if (type.type == TypedValue.TYPE_STRING) {
1135                         mDefaultInterpolatorString = a.getString(attr);
1136                         if (mDefaultInterpolatorString != null) {
1137                             if (mDefaultInterpolatorString.indexOf("/") > 0) {
1138                                 mDefaultInterpolatorID = a.getResourceId(attr, -1);
1139                                 mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID;
1140                             } else {
1141                                 mDefaultInterpolator = SPLINE_STRING;
1142                             }
1143                         }
1144                     } else {
1145                         mDefaultInterpolator = a.getInteger(attr, mDefaultInterpolator);
1146                     }
1147 
1148                 } else if (attr == R.styleable.Transition_duration) {
1149                     mDuration = a.getInt(attr, mDuration);
1150                     if (mDuration < MIN_DURATION) {
1151                         mDuration = MIN_DURATION;
1152                     }
1153                 } else if (attr == R.styleable.Transition_staggered) {
1154                     mStagger = a.getFloat(attr, mStagger);
1155                 } else if (attr == R.styleable.Transition_autoTransition) {
1156                     mAutoTransition = a.getInteger(attr, mAutoTransition);
1157                 } else if (attr == R.styleable.Transition_android_id) {
1158                     mId = a.getResourceId(attr, mId);
1159                 } else if (attr == R.styleable.Transition_transitionDisable) {
1160                     mDisable = a.getBoolean(attr, mDisable);
1161                 } else if (attr == R.styleable.Transition_pathMotionArc) {
1162                     mPathMotionArc = a.getInteger(attr, UNSET);
1163                 } else if (attr == R.styleable.Transition_layoutDuringTransition) {
1164                     mLayoutDuringTransition = a.getInteger(attr, 0);
1165                 } else if (attr == R.styleable.Transition_transitionFlags) {
1166                     mTransitionFlags = a.getInteger(attr, 0);
1167                 }
1168             }
1169             if (mConstraintSetStart == UNSET) {
1170                 mIsAbstract = true;
1171             }
1172         }
1173 
1174     }
1175 
1176     /**
1177      * Create a motion scene.
1178      *
1179      * @param layout Motion layout to which the scene will be set.
1180      */
MotionScene(MotionLayout layout)1181     public MotionScene(MotionLayout layout) {
1182         mMotionLayout = layout;
1183         mViewTransitionController = new ViewTransitionController(layout);
1184     }
1185 
MotionScene(Context context, MotionLayout layout, int resourceID)1186     MotionScene(Context context, MotionLayout layout, int resourceID) {
1187         mMotionLayout = layout;
1188         mViewTransitionController = new ViewTransitionController(layout);
1189 
1190         load(context, resourceID);
1191         mConstraintSetMap.put(R.id.motion_base, new ConstraintSet());
1192         mConstraintSetIdMap.put("motion_base", R.id.motion_base);
1193     }
1194 
1195     /**
1196      * Load a MotionScene   from a MotionScene.xml file
1197      *
1198      * @param context    the context for the inflation
1199      * @param resourceId id of xml file in res/xml/
1200      */
load(Context context, int resourceId)1201     private void load(Context context, int resourceId) {
1202         Resources res = context.getResources();
1203         XmlPullParser parser = res.getXml(resourceId);
1204         try {
1205             Transition transition = null;
1206             for (int eventType = parser.getEventType();
1207                     eventType != XmlResourceParser.END_DOCUMENT;
1208                     eventType = parser.next()) {
1209                 switch (eventType) {
1210                     case XmlResourceParser.START_DOCUMENT:
1211                     case XmlResourceParser.END_TAG:
1212                     case XmlResourceParser.TEXT:
1213                         break;
1214                     case XmlResourceParser.START_TAG:
1215                         String tagName = parser.getName();
1216                         if (DEBUG_DESKTOP) {
1217                             System.out.println("parsing = " + tagName);
1218                         }
1219                         if (DEBUG) {
1220                             Log.v(TAG, "MotionScene ----------- START_TAG " + tagName);
1221                         }
1222                         switch (tagName) {
1223                             case MOTIONSCENE_TAG:
1224                                 parseMotionSceneTags(context, parser);
1225                                 break;
1226                             case TRANSITION_TAG:
1227                                 mTransitionList.add(transition =
1228                                         new Transition(this, context, parser));
1229                                 if (mCurrentTransition == null && !transition.mIsAbstract) {
1230                                     mCurrentTransition = transition;
1231                                     if (mCurrentTransition != null
1232                                             && mCurrentTransition.mTouchResponse != null) {
1233                                         mCurrentTransition.mTouchResponse.setRTL(mRtl);
1234                                     }
1235                                 }
1236                                 if (transition.mIsAbstract) { // global transition only one for now
1237                                     if (transition.mConstraintSetEnd == UNSET) {
1238                                         mDefaultTransition = transition;
1239                                     } else {
1240                                         mAbstractTransitionList.add(transition);
1241                                     }
1242                                     mTransitionList.remove(transition);
1243                                 }
1244                                 break;
1245                             case ONSWIPE_TAG:
1246                                 if (DEBUG || transition == null) {
1247                                     String name = context.getResources()
1248                                             .getResourceEntryName(resourceId);
1249                                     int line = parser.getLineNumber();
1250                                     Log.v(TAG, " OnSwipe (" + name + ".xml:" + line + ")");
1251                                 }
1252                                 if (transition != null) {
1253                                     transition.mTouchResponse =
1254                                             new TouchResponse(context, mMotionLayout, parser);
1255                                 }
1256                                 break;
1257                             case ONCLICK_TAG:
1258                                 if (transition != null) {
1259                                     if (!mMotionLayout.isInEditMode()) {
1260                                         transition.addOnClick(context, parser);
1261                                     }
1262                                 }
1263                                 break;
1264                             case STATESET_TAG:
1265                                 mStateSet = new StateSet(context, parser);
1266                                 break;
1267                             case CONSTRAINTSET_TAG:
1268                                 parseConstraintSet(context, parser);
1269                                 break;
1270                             case INCLUDE_TAG:
1271                             case INCLUDE_TAG_UC:
1272                                 parseInclude(context, parser);
1273                                 break;
1274                             case KEYFRAMESET_TAG:
1275                                 KeyFrames keyFrames = new KeyFrames(context, parser);
1276                                 if (transition != null) {
1277                                     transition.mKeyFramesList.add(keyFrames);
1278                                 }
1279                                 break;
1280                             case VIEW_TRANSITION:
1281                                 ViewTransition viewTransition = new ViewTransition(context, parser);
1282                                 mViewTransitionController.add(viewTransition);
1283                                 break;
1284                             default:
1285                                 if (DEBUG) {
1286                                     Log.v(TAG, getLine(context, resourceId, parser)
1287                                             + "WARNING UNKNOWN ATTRIBUTE " + tagName);
1288                                 }
1289                                 break;
1290                         }
1291 
1292                         break;
1293                 }
1294             }
1295         } catch (XmlPullParserException e) {
1296             if (DEBUG) {
1297                 Log.v(TAG, getLine(context, resourceId, parser) + " " + e.getMessage());
1298             }
1299             Log.e(TAG, "Error parsing resource: " + resourceId, e);
1300         } catch (IOException e) {
1301             if (DEBUG) {
1302                 Log.v(TAG, getLine(context, resourceId, parser) + " " + e.getMessage());
1303             }
1304             Log.e(TAG, "Error parsing resource: " + resourceId, e);
1305         }
1306     }
1307 
parseMotionSceneTags(Context context, XmlPullParser parser)1308     private void parseMotionSceneTags(Context context, XmlPullParser parser) {
1309         AttributeSet attrs = Xml.asAttributeSet(parser);
1310         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MotionScene);
1311         final int count = a.getIndexCount();
1312         for (int i = 0; i < count; i++) {
1313             int attr = a.getIndex(i);
1314             if (attr == R.styleable.MotionScene_defaultDuration) {
1315                 mDefaultDuration = a.getInt(attr, mDefaultDuration);
1316                 if (mDefaultDuration < MIN_DURATION) {
1317                     mDefaultDuration = MIN_DURATION;
1318                 }
1319             } else if (attr == R.styleable.MotionScene_layoutDuringTransition) {
1320                 mLayoutDuringTransition = a.getInteger(attr, LAYOUT_IGNORE_REQUEST);
1321             }
1322         }
1323         a.recycle();
1324     }
1325 
getId(Context context, String idString)1326     private int getId(Context context, String idString) {
1327         int id = UNSET;
1328         if (idString.contains("/")) {
1329             String tmp = idString.substring(idString.indexOf('/') + 1);
1330             id = context.getResources().getIdentifier(tmp, "id", context.getPackageName());
1331             if (DEBUG_DESKTOP) {
1332                 System.out.println("id getMap res = " + id);
1333             }
1334         }
1335         if (id == UNSET) {
1336             if (idString != null && idString.length() > 1) {
1337                 id = Integer.parseInt(idString.substring(1));
1338             } else {
1339                 Log.e(TAG, "error in parsing id");
1340             }
1341         }
1342         return id;
1343     }
1344 
parseInclude(Context context, XmlPullParser mainParser)1345     private void parseInclude(Context context, XmlPullParser mainParser) {
1346         TypedArray a = context
1347                 .obtainStyledAttributes(Xml.asAttributeSet(mainParser), R.styleable.include);
1348         final int count = a.getIndexCount();
1349         for (int i = 0; i < count; i++) {
1350             int attr = a.getIndex(i);
1351             if (attr == R.styleable.include_constraintSet) {
1352                 int resourceId = a.getResourceId(attr, UNSET);
1353                 parseInclude(context, resourceId);
1354             }
1355         }
1356         a.recycle();
1357     }
1358 
parseInclude(Context context, int resourceId)1359     private int parseInclude(Context context, int resourceId) {
1360         Resources res = context.getResources();
1361         XmlPullParser includeParser = res.getXml(resourceId);
1362         try {
1363             for (int eventType = includeParser.getEventType();
1364                     eventType != XmlResourceParser.END_DOCUMENT;
1365                     eventType = includeParser.next()) {
1366                 String tagName = includeParser.getName();
1367                 if (XmlResourceParser.START_TAG == eventType
1368                         && CONSTRAINTSET_TAG.equals(tagName)) {
1369                     return parseConstraintSet(context, includeParser);
1370                 }
1371             }
1372         } catch (XmlPullParserException e) {
1373             if (DEBUG) {
1374                 Log.v(TAG, getLine(context, resourceId, includeParser)
1375                         + " " + e.getMessage());
1376             }
1377             Log.e(TAG, "Error parsing resource: " + resourceId, e);
1378         } catch (IOException e) {
1379             if (DEBUG) {
1380                 Log.v(TAG, getLine(context, resourceId, includeParser)
1381                         + " " + e.getMessage());
1382             }
1383             Log.e(TAG, "Error parsing resource: " + resourceId, e);
1384         }
1385         return UNSET;
1386     }
1387 
parseConstraintSet(Context context, XmlPullParser parser)1388     private int parseConstraintSet(Context context, XmlPullParser parser) {
1389         ConstraintSet set = new ConstraintSet();
1390         set.setForceId(false);
1391         int count = parser.getAttributeCount();
1392         int id = UNSET;
1393         int derivedId = UNSET;
1394         for (int i = 0; i < count; i++) {
1395             String name = parser.getAttributeName(i);
1396             String value = parser.getAttributeValue(i);
1397             if (DEBUG_DESKTOP) {
1398                 System.out.println("id string = " + value);
1399             }
1400             switch (name) {
1401                 case "id":
1402                     id = getId(context, value);
1403                     mConstraintSetIdMap.put(stripID(value), id);
1404                     set.mIdString = Debug.getName(context, id);
1405                     break;
1406                 case "deriveConstraintsFrom":
1407                     derivedId = getId(context, value);
1408                     break;
1409                 case "stateLabels":
1410                     set.setStateLabels(value);
1411                     break;
1412                 case "constraintRotate":
1413                     try {
1414                         set.mRotate = Integer.parseInt(value);
1415                     } catch (NumberFormatException exception) {
1416                         switch (value) {
1417                             case "none":
1418                                 set.mRotate = 0;
1419                                 break;
1420                             case "right":
1421                                 set.mRotate = 1;
1422                                 break;
1423                             case "left":
1424                                 set.mRotate = 2;
1425                                 break;
1426                             case "x_right":
1427                                 set.mRotate = 3;
1428                                 break;
1429                             case "x_left":
1430                                 set.mRotate = 4;
1431                                 break;
1432                         }
1433                     }
1434 
1435 
1436                     break;
1437             }
1438         }
1439         if (id != UNSET) {
1440             if (mMotionLayout.mDebugPath != 0) {
1441                 set.setValidateOnParse(true);
1442             }
1443             set.load(context, parser);
1444             if (derivedId != UNSET) {
1445                 mDeriveMap.put(id, derivedId);
1446             }
1447             mConstraintSetMap.put(id, set);
1448         }
1449         return id;
1450     }
1451 
onLayout(boolean changed, int left, int top, int right, int bottom)1452     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1453     }
1454 
1455     /**
1456      * Get the constraintSet given the id
1457      * @param context
1458      * @param id
1459      * @return
1460      */
getConstraintSet(Context context, String id)1461     public ConstraintSet getConstraintSet(Context context, String id) {
1462         if (DEBUG_DESKTOP) {
1463             System.out.println("id " + id);
1464             System.out.println("size " + mConstraintSetMap.size());
1465         }
1466         for (int i = 0; i < mConstraintSetMap.size(); i++) {
1467             int key = mConstraintSetMap.keyAt(i);
1468             String IdAsString = context.getResources().getResourceName(key);
1469             if (DEBUG_DESKTOP) {
1470                 System.out.println("Id for <" + i + "> is <"
1471                         + IdAsString + "> looking for <" + id + ">");
1472             }
1473             if (id.equals(IdAsString)) {
1474                 return mConstraintSetMap.get(key);
1475             }
1476         }
1477         return null;
1478     }
1479 
getConstraintSet(int id)1480     ConstraintSet getConstraintSet(int id) {
1481         return getConstraintSet(id, -1, -1);
1482     }
1483 
getConstraintSet(int id, int width, int height)1484     ConstraintSet getConstraintSet(int id, int width, int height) {
1485         if (DEBUG_DESKTOP) {
1486             System.out.println("id " + id);
1487             System.out.println("size " + mConstraintSetMap.size());
1488         }
1489         if (mStateSet != null) {
1490             int cid = mStateSet.stateGetConstraintID(id, width, height);
1491             if (cid != -1) {
1492                 id = cid;
1493             }
1494         }
1495         if (mConstraintSetMap.get(id) == null) {
1496             Log.e(TAG, "Warning could not find ConstraintSet id/"
1497                     + Debug.getName(mMotionLayout.getContext(), id) + " In MotionScene");
1498             return mConstraintSetMap.get(mConstraintSetMap.keyAt(0));
1499         }
1500         return mConstraintSetMap.get(id);
1501     }
1502 
1503     /**
1504      * Maps the Constraint set to the id.
1505      *
1506      * @param id  - unique id to represent the ConstraintSet
1507      * @param set - ConstraintSet to be represented with the id.
1508      */
setConstraintSet(int id, ConstraintSet set)1509     public void setConstraintSet(int id, ConstraintSet set) {
1510         mConstraintSetMap.put(id, set);
1511     }
1512 
1513     /**
1514      * provides the key frames & CycleFrames to the motion view to
1515      *
1516      * @param motionController
1517      */
getKeyFrames(MotionController motionController)1518     public void getKeyFrames(MotionController motionController) {
1519         if (mCurrentTransition == null) {
1520             if (mDefaultTransition != null) {
1521                 for (KeyFrames keyFrames : mDefaultTransition.mKeyFramesList) {
1522                     keyFrames.addFrames(motionController);
1523                 }
1524             }
1525             return;
1526         }
1527         for (KeyFrames keyFrames : mCurrentTransition.mKeyFramesList) {
1528             keyFrames.addFrames(motionController);
1529         }
1530     }
1531 
1532     /**
1533      * get key frame
1534      *
1535      * @param context
1536      * @param type
1537      * @param target
1538      * @param position
1539      * @return Key Object
1540      */
getKeyFrame(Context context, int type, int target, int position)1541     Key getKeyFrame(Context context, int type, int target, int position) {
1542         if (mCurrentTransition == null) {
1543             return null;
1544         }
1545         for (KeyFrames keyFrames : mCurrentTransition.mKeyFramesList) {
1546             for (Integer integer : keyFrames.getKeys()) {
1547                 if (target == integer) {
1548                     ArrayList<Key> keys = keyFrames.getKeyFramesForView(integer);
1549                     for (Key key : keys) {
1550                         if (key.mFramePosition == position) {
1551                             if (key.mType == type) {
1552                                 return key;
1553                             }
1554                         }
1555                     }
1556                 }
1557             }
1558 
1559         }
1560         return null;
1561     }
1562 
getTransitionDirection(int stateId)1563     int getTransitionDirection(int stateId) {
1564         for (Transition transition : mTransitionList) {
1565             if (transition.mConstraintSetStart == stateId) {
1566                 return TRANSITION_BACKWARD;
1567             }
1568         }
1569         return TRANSITION_FORWARD;
1570     }
1571 
1572     /**
1573      * Returns true if the view has a keyframe defined at the given position
1574      *
1575      * @param view
1576      * @param position
1577      * @return true if a keyframe exists, false otherwise
1578      */
hasKeyFramePosition(View view, int position)1579     boolean hasKeyFramePosition(View view, int position) {
1580         if (mCurrentTransition == null) {
1581             return false;
1582         }
1583         for (KeyFrames keyFrames : mCurrentTransition.mKeyFramesList) {
1584             ArrayList<Key> framePoints = keyFrames.getKeyFramesForView(view.getId());
1585             for (Key framePoint : framePoints) {
1586                 if (framePoint.mFramePosition == position) {
1587                     return true;
1588                 }
1589             }
1590         }
1591         return false;
1592     }
1593 
1594     /**
1595      * Set a keyFrame on the current Transition
1596      * @param view
1597      * @param position
1598      * @param name
1599      * @param value
1600      */
setKeyframe(View view, int position, String name, Object value)1601     public void setKeyframe(View view, int position, String name, Object value) {
1602         if (DEBUG) {
1603             System.out.println("setKeyframe for pos " + position
1604                     + " name <" + name + "> value: " + value);
1605         }
1606         if (mCurrentTransition == null) {
1607             return;
1608         }
1609         for (KeyFrames keyFrames : mCurrentTransition.mKeyFramesList) {
1610             if (DEBUG) {
1611                 System.out.println("key frame " + keyFrames);
1612             }
1613             ArrayList<Key> framePoints = keyFrames.getKeyFramesForView(view.getId());
1614             if (DEBUG) {
1615                 System.out.println("key frame has " + framePoints.size() + " frame points");
1616             }
1617             for (Key framePoint : framePoints) {
1618                 if (DEBUG) {
1619                     System.out.println("framePoint pos: " + framePoint.mFramePosition);
1620                 }
1621                 if (framePoint.mFramePosition == position) {
1622                     float v = 0;
1623                     if (value != null) {
1624                         v = ((Float) value).floatValue();
1625                         if (DEBUG) {
1626                             System.out.println("value: " + v);
1627                         }
1628                     } else {
1629                         if (DEBUG) {
1630                             System.out.println("value was null!!!");
1631                         }
1632                     }
1633                     if (v == 0) {
1634                         v = 0.01f;
1635                     }
1636                 }
1637             }
1638         }
1639     }
1640 
1641     /**
1642      * Get the path percent  (Non functional currently)
1643      * @param view
1644      * @param position
1645      * @return
1646      */
getPathPercent(View view, int position)1647     public float getPathPercent(View view, int position) {
1648         return 0;
1649     }
1650 
1651     //////////////////////////////////////////////////////////
1652     // touch handling
1653     ///////////////////////////////////////////////////////////
supportTouch()1654     boolean supportTouch() {
1655         for (Transition transition : mTransitionList) {
1656             if (transition.mTouchResponse != null) {
1657                 return true;
1658             }
1659         }
1660         return mCurrentTransition != null && mCurrentTransition.mTouchResponse != null;
1661     }
1662 
1663     float mLastTouchX, mLastTouchY;
1664 
processTouchEvent(MotionEvent event, int currentState, MotionLayout motionLayout)1665     void processTouchEvent(MotionEvent event, int currentState, MotionLayout motionLayout) {
1666         if (DEBUG) {
1667             Log.v(TAG, Debug.getLocation() + " processTouchEvent");
1668         }
1669         RectF cache = new RectF();
1670         if (mVelocityTracker == null) {
1671             mVelocityTracker = mMotionLayout.obtainVelocityTracker();
1672         }
1673         mVelocityTracker.addMovement(event);
1674         if (DEBUG) {
1675             float time = (event.getEventTime() % 100000) / 1000f;
1676             float x = event.getRawX();
1677             float y = event.getRawY();
1678             Log.v(TAG, " " + time + "  processTouchEvent "
1679                     + "state=" + Debug.getState(motionLayout, currentState)
1680                     + "  " + Debug.getActionType(event) + " " + x
1681                     + ", " + y + " \t " + motionLayout.getProgress());
1682         }
1683 
1684         if (currentState != -1) {
1685             RectF region;
1686             switch (event.getAction()) {
1687                 case MotionEvent.ACTION_DOWN:
1688                     mLastTouchX = event.getRawX();
1689                     mLastTouchY = event.getRawY();
1690                     mLastTouchDown = event;
1691                     mIgnoreTouch = false;
1692                     if (mCurrentTransition.mTouchResponse != null) {
1693                         region = mCurrentTransition.mTouchResponse
1694                                 .getLimitBoundsTo(mMotionLayout, cache);
1695                         if (region != null
1696                                 && !region.contains(mLastTouchDown.getX(), mLastTouchDown.getY())) {
1697                             mLastTouchDown = null;
1698                             mIgnoreTouch = true;
1699                             return;
1700                         }
1701                         region = mCurrentTransition.mTouchResponse
1702                                 .getTouchRegion(mMotionLayout, cache);
1703                         if (region != null
1704                                 && !region.contains(mLastTouchDown.getX(),
1705                                 mLastTouchDown.getY())) {
1706                             mMotionOutsideRegion = true;
1707                         } else {
1708                             mMotionOutsideRegion = false;
1709                         }
1710                         mCurrentTransition.mTouchResponse.setDown(mLastTouchX, mLastTouchY);
1711                     }
1712                     if (DEBUG) {
1713                         Log.v(TAG, "----- ACTION_DOWN " + mLastTouchX + "," + mLastTouchY);
1714                     }
1715                     return;
1716                 case MotionEvent.ACTION_MOVE:
1717                     if (mIgnoreTouch) {
1718                         break;
1719                     }
1720                     float dy = event.getRawY() - mLastTouchY;
1721                     float dx = event.getRawX() - mLastTouchX;
1722                     if (DEBUG) {
1723                         Log.v(TAG, "----- ACTION_MOVE " + dx + "," + dy);
1724                     }
1725                     if ((dx == 0.0 && dy == 0.0) || mLastTouchDown == null) {
1726                         return;
1727                     }
1728 
1729                     Transition transition =
1730                             bestTransitionFor(currentState, dx, dy, mLastTouchDown);
1731                     if (DEBUG) {
1732                         Log.v(TAG, Debug.getLocation() + " best Transition For "
1733                                 + dx + "," + dy + " "
1734                                 + ((transition == null) ? null
1735                                         : transition.debugString(mMotionLayout.getContext())));
1736                     }
1737                     if (transition != null) {
1738 
1739                         motionLayout.setTransition(transition);
1740                         region = mCurrentTransition
1741                                 .mTouchResponse.getTouchRegion(mMotionLayout, cache);
1742                         mMotionOutsideRegion = region != null
1743                                 && !region.contains(mLastTouchDown.getX(), mLastTouchDown.getY());
1744                         mCurrentTransition.mTouchResponse.setUpTouchEvent(mLastTouchX, mLastTouchY);
1745                     }
1746             }
1747         }
1748         if (mIgnoreTouch) {
1749             return;
1750         }
1751         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null
1752                 && !mMotionOutsideRegion) {
1753             mCurrentTransition.mTouchResponse.processTouchEvent(event,
1754                     mVelocityTracker, currentState, this);
1755         }
1756 
1757         mLastTouchX = event.getRawX();
1758         mLastTouchY = event.getRawY();
1759 
1760         if (event.getAction() == MotionEvent.ACTION_UP) {
1761             if (mVelocityTracker != null) {
1762                 mVelocityTracker.recycle();
1763                 mVelocityTracker = null;
1764                 if (motionLayout.mCurrentState != UNSET) {
1765                     autoTransition(motionLayout, motionLayout.mCurrentState);
1766                 }
1767             }
1768         }
1769     }
1770 
processScrollMove(float dx, float dy)1771     void processScrollMove(float dx, float dy) {
1772         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1773             mCurrentTransition.mTouchResponse.scrollMove(dx, dy);
1774         }
1775     }
1776 
processScrollUp(float dx, float dy)1777     void processScrollUp(float dx, float dy) {
1778         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1779             mCurrentTransition.mTouchResponse.scrollUp(dx, dy);
1780         }
1781     }
1782 
1783     /**
1784      * Calculate if a drag in this direction results in an increase or decrease in progress.
1785      *
1786      * @param dx drag direction in x
1787      * @param dy drag direction in y
1788      * @return change in progress given that dx and dy
1789      */
getProgressDirection(float dx, float dy)1790     float getProgressDirection(float dx, float dy) {
1791         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1792             return mCurrentTransition.mTouchResponse.getProgressDirection(dx, dy);
1793         }
1794         return 0;
1795     }
1796 
1797     /////////////////////////////////////////////////////////////
1798 
getStartId()1799     int getStartId() {
1800         if (mCurrentTransition == null) {
1801             return -1;
1802         }
1803         return mCurrentTransition.mConstraintSetStart;
1804     }
1805 
getEndId()1806     int getEndId() {
1807         if (mCurrentTransition == null) {
1808             return -1;
1809         }
1810         return mCurrentTransition.mConstraintSetEnd;
1811     }
1812 
1813     static final int EASE_IN_OUT = 0;
1814     static final int EASE_IN = 1;
1815     static final int EASE_OUT = 2;
1816     static final int LINEAR = 3;
1817     static final int BOUNCE = 4;
1818     static final int OVERSHOOT = 5;
1819     static final int ANTICIPATE = 6;
1820 
1821     /**
1822      * Get the interpolator define for the current Transition
1823      * @return
1824      */
getInterpolator()1825     public Interpolator getInterpolator() {
1826         switch (mCurrentTransition.mDefaultInterpolator) {
1827             case SPLINE_STRING:
1828                 final Easing easing = Easing
1829                         .getInterpolator(mCurrentTransition.mDefaultInterpolatorString);
1830                 return new Interpolator() {
1831                     @Override
1832                     public float getInterpolation(float v) {
1833                         return (float) easing.get(v);
1834                     }
1835                 };
1836             case INTERPOLATOR_REFERENCE_ID:
1837                 return AnimationUtils.loadInterpolator(mMotionLayout.getContext(),
1838                         mCurrentTransition.mDefaultInterpolatorID);
1839             case EASE_IN_OUT:
1840                 return new AccelerateDecelerateInterpolator();
1841             case EASE_IN:
1842                 return new AccelerateInterpolator();
1843             case EASE_OUT:
1844                 return new DecelerateInterpolator();
1845             case LINEAR:
1846                 return null;
1847             case ANTICIPATE:
1848                 return new AnticipateInterpolator();
1849             case OVERSHOOT:
1850                 return new OvershootInterpolator();
1851             case BOUNCE:
1852                 return new BounceInterpolator();
1853         }
1854         return null;
1855     }
1856 
1857     /**
1858      * Get Duration of the current transition.
1859      *
1860      * @return duration in milliseconds
1861      */
1862     public int getDuration() {
1863         if (mCurrentTransition != null) {
1864             return mCurrentTransition.mDuration;
1865         }
1866         return mDefaultDuration;
1867     }
1868 
1869     /**
1870      * Sets the duration of the current transition or the default if there is no current transition
1871      *
1872      * @param duration in milliseconds
1873      */
1874     public void setDuration(int duration) {
1875         if (mCurrentTransition != null) {
1876             mCurrentTransition.setDuration(duration);
1877         } else {
1878             mDefaultDuration = duration;
1879         }
1880     }
1881 
1882     /**
1883      * The transition arc path mode
1884      * @return
1885      */
1886     public int gatPathMotionArc() {
1887         return (mCurrentTransition != null) ? mCurrentTransition.mPathMotionArc : UNSET;
1888     }
1889 
1890     /**
1891      * Get the staggered value of the current transition.
1892      * Will default to 0 staggered if there is no current transition.
1893      *
1894      * @return
1895      */
1896     public float getStaggered() {
1897         if (mCurrentTransition != null) {
1898             return mCurrentTransition.mStagger;
1899         }
1900         return 0;
1901     }
1902 
1903     float getMaxAcceleration() {
1904         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1905             return mCurrentTransition.mTouchResponse.getMaxAcceleration();
1906         }
1907         return 0;
1908     }
1909 
1910     float getMaxVelocity() {
1911         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1912             return mCurrentTransition.mTouchResponse.getMaxVelocity();
1913         }
1914         return 0;
1915     }
1916 
1917     float getSpringStiffiness() {
1918         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1919             return mCurrentTransition.mTouchResponse.getSpringStiffness();
1920         }
1921         return 0;
1922     }
1923 
1924     float getSpringMass() {
1925         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1926             return mCurrentTransition.mTouchResponse.getSpringMass();
1927         }
1928         return 0;
1929     }
1930 
1931     float getSpringDamping() {
1932         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1933             return mCurrentTransition.mTouchResponse.getSpringDamping();
1934         }
1935         return 0;
1936     }
1937 
1938     float getSpringStopThreshold() {
1939         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1940             return mCurrentTransition.mTouchResponse.getSpringStopThreshold();
1941         }
1942         return 0;
1943     }
1944     int getSpringBoundary() {
1945         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1946             return mCurrentTransition.mTouchResponse.getSpringBoundary();
1947         }
1948         return 0;
1949     }
1950     int getAutoCompleteMode() {
1951         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1952             return mCurrentTransition.mTouchResponse.getAutoCompleteMode();
1953         }
1954         return 0;
1955     }
1956     void setupTouch() {
1957         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1958             mCurrentTransition.mTouchResponse.setupTouch();
1959         }
1960     }
1961 
1962     boolean getMoveWhenScrollAtTop() {
1963         if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
1964             return mCurrentTransition.mTouchResponse.getMoveWhenScrollAtTop();
1965         }
1966         return false;
1967     }
1968 
1969     /**
1970      * read the constraints from the inflation of the ConstraintLayout
1971      * If the constraintset does not contain information about a view this information is used
1972      * as a "fallback" position.
1973      *
1974      * @param motionLayout
1975      */
1976     void readFallback(MotionLayout motionLayout) {
1977 
1978         for (int i = 0; i < mConstraintSetMap.size(); i++) {
1979             int key = mConstraintSetMap.keyAt(i);
1980             if (hasCycleDependency(key)) {
1981                 Log.e(TAG, "Cannot be derived from yourself");
1982                 return;
1983             }
1984             readConstraintChain(key, motionLayout);
1985         }
1986     }
1987 
1988     /**
1989      * This is brute force but the number of ConstraintSets is typically very small (< 5)
1990      *
1991      * @param key
1992      * @return
1993      */
1994     private boolean hasCycleDependency(int key) {
1995         int derived = mDeriveMap.get(key);
1996         int len = mDeriveMap.size();
1997         while (derived > 0) {
1998             if (derived == key) {
1999                 return true;
2000             }
2001             if (len-- < 0) {
2002                 return true;
2003             }
2004             derived = mDeriveMap.get(derived);
2005         }
2006         return false;
2007     }
2008 
2009     /**
2010      * Recursive descent of the deriveConstraintsFrom tree reading the motionLayout if
2011      * needed.
2012      *
2013      * @param key
2014      */
2015     private void readConstraintChain(int key, MotionLayout motionLayout) {
2016         ConstraintSet cs = mConstraintSetMap.get(key);
2017         cs.derivedState = cs.mIdString;
2018         int derivedFromId = mDeriveMap.get(key);
2019         if (derivedFromId > 0) {
2020             readConstraintChain(derivedFromId, motionLayout);
2021             ConstraintSet derivedFrom = mConstraintSetMap.get(derivedFromId);
2022             if (derivedFrom == null) {
2023                 Log.e(TAG, "ERROR! invalid deriveConstraintsFrom: @id/"
2024                         + Debug.getName(mMotionLayout.getContext(), derivedFromId));
2025                 return;
2026             }
2027             cs.derivedState += "/" + derivedFrom.derivedState;
2028             cs.readFallback(derivedFrom);
2029         } else {
2030             cs.derivedState += "  layout";
2031             cs.readFallback(motionLayout);
2032         }
2033         cs.applyDeltaFrom(cs);
2034     }
2035 
2036     /**
2037      * Utility to strip the @id/ from an id
2038      * @param id
2039      * @return
2040      */
2041     public static String stripID(String id) {
2042         if (id == null) {
2043             return "";
2044         }
2045         int index = id.indexOf('/');
2046         if (index < 0) {
2047             return id;
2048         }
2049         return id.substring(index + 1);
2050     }
2051 
2052     /**
2053      * Used at design time
2054      *
2055      * @param id
2056      * @return
2057      */
2058     public int lookUpConstraintId(String id) {
2059         Integer boxed = mConstraintSetIdMap.get(id);
2060         if (boxed == null) {
2061             return 0;
2062         } else {
2063             return boxed;
2064         }
2065     }
2066 
2067     /**
2068      * used at design time
2069      *
2070      * @return
2071      */
2072     public String lookUpConstraintName(int id) {
2073         for (Map.Entry<String, Integer> entry : mConstraintSetIdMap.entrySet()) {
2074             Integer boxed = entry.getValue();
2075             if (boxed == null) {
2076                 continue;
2077             }
2078 
2079             if (boxed == id) {
2080                 return entry.getKey();
2081             }
2082         }
2083         return null;
2084     }
2085 
2086     /**
2087      * this allow disabling autoTransitions to prevent design surface from being in undefined states
2088      *
2089      * @param disable
2090      */
2091     public void disableAutoTransition(boolean disable) {
2092         mDisableAutoTransition = disable;
2093     }
2094 
2095     /**
2096      * Construct a user friendly error string
2097      *
2098      * @param context    the context
2099      * @param resourceId the xml being parsed
2100      * @param pullParser the XML parser
2101      * @return
2102      */
2103     static String getLine(Context context, int resourceId, XmlPullParser pullParser) {
2104         return ".(" + Debug.getName(context, resourceId) + ".xml:" + pullParser.getLineNumber()
2105                 + ") \"" + pullParser.getName() + "\"";
2106     }
2107 }
2108