MotionScene.java

/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.constraintlayout.motion.widget;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.util.Xml;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.BounceInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.OvershootInterpolator;

import androidx.constraintlayout.core.motion.utils.Easing;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.R;
import androidx.constraintlayout.widget.StateSet;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The information to transition between multiple ConstraintSets
 * This Class is meant to be used from XML
 *
 */
public class MotionScene {
    private static final String TAG = "MotionScene";
    private static final boolean DEBUG = false;
    private static final int MIN_DURATION = 8;
    static final int TRANSITION_BACKWARD = 0;
    static final int TRANSITION_FORWARD = 1;
    private static final int SPLINE_STRING = -1;
    private static final int INTERPOLATOR_REFERENCE_ID = -2;
    public static final int UNSET = -1;
    private final MotionLayout mMotionLayout;
    StateSet mStateSet = null;
    Transition mCurrentTransition = null;
    private boolean mDisableAutoTransition = false;
    private ArrayList<Transition> mTransitionList = new ArrayList<>();
    private Transition mDefaultTransition = null;
    private ArrayList<Transition> mAbstractTransitionList = new ArrayList<>();

    private SparseArray<ConstraintSet> mConstraintSetMap = new SparseArray<>();
    private HashMap<String, Integer> mConstraintSetIdMap = new HashMap<>();
    private SparseIntArray mDeriveMap = new SparseIntArray();
    private static final boolean DEBUG_DESKTOP = false;
    private int mDefaultDuration = 400;
    private int mLayoutDuringTransition = 0;
    public static final int LAYOUT_IGNORE_REQUEST = 0;
    public static final int LAYOUT_HONOR_REQUEST = 1;
    public static final int LAYOUT_CALL_MEASURE = 2;

    private MotionEvent mLastTouchDown;
    private boolean mIgnoreTouch = false;
    private boolean mMotionOutsideRegion = false;
    private MotionLayout.MotionTracker mVelocityTracker; // used to support fling
    private boolean mRtl;
    private static final String MOTIONSCENE_TAG = "MotionScene";
    private static final String TRANSITION_TAG = "Transition";
    private static final String ONSWIPE_TAG = "OnSwipe";
    private static final String ONCLICK_TAG = "OnClick";
    private static final String STATESET_TAG = "StateSet";
    private static final String INCLUDE_TAG_UC = "Include";
    private static final String INCLUDE_TAG = "include";
    private static final String KEYFRAMESET_TAG = "KeyFrameSet";
    private static final String CONSTRAINTSET_TAG = "ConstraintSet";
    private static final String VIEW_TRANSITION = "ViewTransition";
    final ViewTransitionController mViewTransitionController;

    /**
     * Set the transition between two constraint set / states.
     * The transition will get created between the two sets
     * if it doesn't exist already.
     *
     * @param beginId id of the start constraint set or state
     * @param endId   id of the end constraint set or state
     */
    void setTransition(int beginId, int endId) {
        int start = beginId;
        int end = endId;
        if (mStateSet != null) {
            int tmp = mStateSet.stateGetConstraintID(beginId, -1, -1);
            if (tmp != -1) {
                start = tmp;
            }
            tmp = mStateSet.stateGetConstraintID(endId, -1, -1);
            if (tmp != -1) {
                end = tmp;
            }
        }
        if (DEBUG) {
            Log.v(TAG, Debug.getLocation() + " setTransition "
                    + Debug.getName(mMotionLayout.getContext(), beginId) + " -> "
                    + Debug.getName(mMotionLayout.getContext(), endId));
        }
        if (mCurrentTransition != null) {
            if (mCurrentTransition.mConstraintSetEnd == endId
                    && mCurrentTransition.mConstraintSetStart == beginId) {
                return;
            }
        }
        for (Transition transition : mTransitionList) {
            if ((transition.mConstraintSetEnd == end
                    && transition.mConstraintSetStart == start)
                    || (transition.mConstraintSetEnd == endId
                    && transition.mConstraintSetStart == beginId)) {
                if (DEBUG) {
                    Log.v(TAG, Debug.getLocation() + " found transition  "
                            + Debug.getName(mMotionLayout.getContext(), beginId) + " -> "
                            + Debug.getName(mMotionLayout.getContext(), endId));
                }
                mCurrentTransition = transition;
                if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
                    mCurrentTransition.mTouchResponse.setRTL(mRtl);
                }
                return;
            }
        }
        // No transition defined for this so we will create one?
        Transition matchTransition = mDefaultTransition;
        for (Transition transition : mAbstractTransitionList) {
            if (transition.mConstraintSetEnd == endId) {
                matchTransition = transition;
            }
        }
        Transition t = new Transition(this, matchTransition);

        t.mConstraintSetStart = start;
        t.mConstraintSetEnd = end;
        if (start != UNSET) {
            mTransitionList.add(t);
        }
        mCurrentTransition = t;
    }

    /**
     * Add a transition to the motion scene. If a transition with the same id already exists
     * in the scene, the new transition will replace the existing one.
     *
     * @throws IllegalArgumentException if the transition does not have an id.
     */
    public void addTransition(Transition transition) {
        int index = getIndex(transition);
        if (index == -1) {
            mTransitionList.add(transition);
        } else {
            mTransitionList.set(index, transition);
        }
    }

    /**
     * Remove the transition with the matching id from the motion scene. If no matching transition
     * is found, it does nothing.
     *
     * @throws IllegalArgumentException if the transition does not have an id.
     */
    public void removeTransition(Transition transition) {
        int index = getIndex(transition);
        if (index != -1) {
            mTransitionList.remove(index);
        }
    }

    /**
     * @return the index in the transition list. -1 if transition wasn't found.
     */
    private int getIndex(Transition transition) {
        int id = transition.mId;
        if (id == UNSET) {
            throw new IllegalArgumentException("The transition must have an id");
        }

        int index = 0;
        for (; index < mTransitionList.size(); index++) {
            if (mTransitionList.get(index).mId == id) {
                return index;
            }
        }

        return -1;
    }

    /**
     * @return true if the layout is valid for the scene. False otherwise. Use it for the debugging
     * purposes.
     */
    public boolean validateLayout(MotionLayout layout) {
        return (layout == mMotionLayout && layout.mScene == this);
    }

    /**
     * Set the transition to be the current transition of the motion scene.
     *
     * @param transition a transition to be set. The transition must exist within the motion scene.
     *                   (e.g. {@link #addTransition(Transition)})
     */
    public void setTransition(Transition transition) {
        mCurrentTransition = transition;
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            mCurrentTransition.mTouchResponse.setRTL(mRtl);
        }
    }

    private int getRealID(int stateId) {
        if (mStateSet != null) {
            int tmp = mStateSet.stateGetConstraintID(stateId, -1, -1);
            if (tmp != -1) {
                return tmp;
            }
        }
        return stateId;
    }

    /**
     * Get all transitions that include this state
     * @param stateId
     * @return
     */
    public List<Transition> getTransitionsWithState(int stateId) {
        stateId = getRealID(stateId);
        ArrayList<Transition> ret = new ArrayList<>();
        for (Transition transition : mTransitionList) {
            if (transition.mConstraintSetStart == stateId
                    || transition.mConstraintSetEnd == stateId) {
                ret.add(transition);
            }

        }
        return ret;
    }

    /**
     * Add all on click listeners for the current state
     * @param motionLayout
     * @param currentState
     */
    public void addOnClickListeners(MotionLayout motionLayout, int currentState) {
        // remove all on clicks listeners
        for (Transition transition : mTransitionList) {
            if (transition.mOnClicks.size() > 0) {
                for (Transition.TransitionOnClick onClick : transition.mOnClicks) {
                    onClick.removeOnClickListeners(motionLayout);
                }
            }
        }
        for (Transition transition : mAbstractTransitionList) {
            if (transition.mOnClicks.size() > 0) {
                for (Transition.TransitionOnClick onClick : transition.mOnClicks) {
                    onClick.removeOnClickListeners(motionLayout);
                }
            }
        }
        // add back all the listeners that are needed
        for (Transition transition : mTransitionList) {
            if (transition.mOnClicks.size() > 0) {
                for (Transition.TransitionOnClick onClick : transition.mOnClicks) {
                    onClick.addOnClickListeners(motionLayout, currentState, transition);
                }
            }
        }
        for (Transition transition : mAbstractTransitionList) {
            if (transition.mOnClicks.size() > 0) {
                for (Transition.TransitionOnClick onClick : transition.mOnClicks) {
                    onClick.addOnClickListeners(motionLayout, currentState, transition);
                }
            }
        }
    }

    /**
     * Find the best transition for the motion
     * @param currentState
     * @param dx drag delta x
     * @param dy drag delta y
     * @param lastTouchDown
     * @return
     */
    public Transition bestTransitionFor(int currentState,
                                        float dx,
                                        float dy,
                                        MotionEvent lastTouchDown) {
        List<Transition> candidates = null;
        if (currentState != -1) {
            candidates = getTransitionsWithState(currentState);
            float max = 0;
            Transition best = null;
            RectF cache = new RectF();
            for (Transition transition : candidates) {
                if (transition.mDisable) {
                    continue;
                }
                if (transition.mTouchResponse != null) {
                    transition.mTouchResponse.setRTL(mRtl);
                    RectF region = transition.mTouchResponse.getTouchRegion(mMotionLayout, cache);
                    if (region != null && lastTouchDown != null
                            && !region.contains(lastTouchDown.getX(), lastTouchDown.getY())) {
                        continue;
                    }
                    region = transition.mTouchResponse.getLimitBoundsTo(mMotionLayout, cache);
                    if (region != null && lastTouchDown != null
                            && !region.contains(lastTouchDown.getX(), lastTouchDown.getY())) {
                        continue;
                    }

                    float val = transition.mTouchResponse.dot(dx, dy);
                    if (transition.mTouchResponse.mIsRotateMode && lastTouchDown != null) {
                        float startX = lastTouchDown.getX()
                                - transition.mTouchResponse.mRotateCenterX;
                        float startY = lastTouchDown.getY()
                                - transition.mTouchResponse.mRotateCenterY;
                        float endX = dx + startX;
                        float endY = dy + startY;
                        double endAngle = Math.atan2(endY, endX);
                        double startAngle = Math.atan2(startX, startY);
                        val = (float) (endAngle - startAngle) * 10;
                    }
                    if (transition.mConstraintSetEnd == currentState) { // flip because backwards
                        val *= -1;
                    } else {
                        val *= 1.1f; // slightly bias towards the transition which is start over end
                    }

                    if (val > max) {
                        max = val;
                        best = transition;
                    }
                }
            }
            if (DEBUG) {
                if (best != null) {
                    Log.v(TAG, Debug.getLocation() + "  ### BEST ----- "
                            + best.debugString(mMotionLayout.getContext()) + " ----");
                } else {
                    Log.v(TAG, Debug.getLocation() + "  ### BEST ----- " + null + " ----");

                }
            }
            return best;
        }
        return mCurrentTransition;
    }

    /**
     * Get list of Transitions know to the system
     * @return
     */
    public ArrayList<Transition> getDefinedTransitions() {
        return mTransitionList;
    }

    /**
     * Find the transition based on the id
     * @param id
     * @return
     */
    public Transition getTransitionById(int id) {
        for (Transition transition : mTransitionList) {
            if (transition.mId == id) {
                return transition;
            }
        }
        return null;
    }

    /**
     * Get the list of all Constraint Sets Know to the system
     * @return
     */
    public int[] getConstraintSetIds() {
        int[] ids = new int[mConstraintSetMap.size()];
        for (int i = 0; i < ids.length; i++) {
            ids[i] = mConstraintSetMap.keyAt(i);
        }
        return ids;
    }

    /**
     * Get the id's of all constraintSets with the matching types
     * @return
     */
    public int[] getMatchingStateLabels(String ... types) {
        int[] ids = new int[mConstraintSetMap.size()];

        int count = 0;

        for (int i = 0; i < ids.length; i++) {
            ConstraintSet set = mConstraintSetMap.valueAt(i);
            int id = mConstraintSetMap.keyAt(i);
            if (set.matchesLabels(types)) {
                @SuppressWarnings("unused") String[] s = set.getStateLabels();
                ids[count++] = id;
            }
        }
        return Arrays.copyOf(ids, count);
    }

    /**
     * This will launch a transition to another state if an autoTransition is enabled on
     * a Transition that matches the current state.
     *
     * @param motionLayout
     * @param currentState
     * @return
     *
     */
    boolean autoTransition(MotionLayout motionLayout, int currentState) {
        if (isProcessingTouch()) {
            return false;
        }
        if (mDisableAutoTransition) {
            return false;
        }

        for (Transition transition : mTransitionList) {
            if (transition.mAutoTransition == Transition.AUTO_NONE) {
                continue;
            }
            if (mCurrentTransition == transition
                    && mCurrentTransition.isTransitionFlag(Transition.TRANSITION_FLAG_INTRA_AUTO)) {
                continue;
            }
            if (currentState == transition.mConstraintSetStart && (
                    transition.mAutoTransition == Transition.AUTO_ANIMATE_TO_END
                            || transition.mAutoTransition == Transition.AUTO_JUMP_TO_END)) {
                motionLayout.setState(MotionLayout.TransitionState.FINISHED);
                motionLayout.setTransition(transition);
                if (transition.mAutoTransition == Transition.AUTO_ANIMATE_TO_END) {
                    motionLayout.transitionToEnd();
                    motionLayout.setState(MotionLayout.TransitionState.SETUP);
                    motionLayout.setState(MotionLayout.TransitionState.MOVING);
                } else {
                    motionLayout.setProgress(1);
                    motionLayout.evaluate(true);
                    motionLayout.setState(MotionLayout.TransitionState.SETUP);
                    motionLayout.setState(MotionLayout.TransitionState.MOVING);
                    motionLayout.setState(MotionLayout.TransitionState.FINISHED);
                    motionLayout.onNewStateAttachHandlers();
                }
                return true;
            }
            if (currentState == transition.mConstraintSetEnd && (
                    transition.mAutoTransition == Transition.AUTO_ANIMATE_TO_START
                            || transition.mAutoTransition == Transition.AUTO_JUMP_TO_START)) {
                motionLayout.setState(MotionLayout.TransitionState.FINISHED);
                motionLayout.setTransition(transition);
                if (transition.mAutoTransition == Transition.AUTO_ANIMATE_TO_START) {
                    motionLayout.transitionToStart();
                    motionLayout.setState(MotionLayout.TransitionState.SETUP);
                    motionLayout.setState(MotionLayout.TransitionState.MOVING);
                } else {
                    motionLayout.setProgress(0);
                    motionLayout.evaluate(true);
                    motionLayout.setState(MotionLayout.TransitionState.SETUP);
                    motionLayout.setState(MotionLayout.TransitionState.MOVING);
                    motionLayout.setState(MotionLayout.TransitionState.FINISHED);
                    motionLayout.onNewStateAttachHandlers();
                }
                return true;
            }
        }
        return false;
    }

    private boolean isProcessingTouch() {
        return (mVelocityTracker != null);
    }

    /**
     * Set Right to left
     * @param rtl
     */
    public void setRtl(boolean rtl) {
        mRtl = rtl;
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            mCurrentTransition.mTouchResponse.setRTL(mRtl);
        }
    }

    /**
     * Apply the viewTransition on the list of views
     * @param id
     * @param view
     */
    public void viewTransition(int id, View... view) {
        mViewTransitionController.viewTransition(id, view);
    }

    /**
     * Enable this viewTransition
     * @param id of viewTransition
     * @param enable
     */
    public void enableViewTransition(int id, boolean enable) {
        mViewTransitionController.enableViewTransition(id, enable);
    }

    /**
     * Is this view transition enabled
     * @param id of viewTransition
     * @return
     */
    public boolean isViewTransitionEnabled(int id) {
        return mViewTransitionController.isViewTransitionEnabled(id);
    }

    /**
     * Apply a view transition to the MotionController
     * @param viewTransitionId of viewTransition
     * @param motionController
     * @return
     */
    public boolean applyViewTransition(int viewTransitionId, MotionController motionController) {
        return mViewTransitionController.applyViewTransition(viewTransitionId, motionController);
    }

///////////////////////////////////////////////////////////////////////////////
// ====================== Transition ==========================================

    /**
     * Transition defines the interaction from one state to another.
     * With out a Transition object Transition between two stats involves strictly linear
     * interpolation
     */
    public static class Transition {
        private int mId = UNSET;
        private boolean mIsAbstract = false;
        private int mConstraintSetEnd = -1;
        private int mConstraintSetStart = -1;
        private int mDefaultInterpolator = 0;
        private String mDefaultInterpolatorString = null;
        private int mDefaultInterpolatorID = -1;
        private int mDuration = 400;
        private float mStagger = 0.0f;
        private final MotionScene mMotionScene;
        private ArrayList<KeyFrames> mKeyFramesList = new ArrayList<>();
        private TouchResponse mTouchResponse = null;
        private ArrayList<TransitionOnClick> mOnClicks = new ArrayList<>();
        private int mAutoTransition = 0;
        public static final int AUTO_NONE = 0;
        public static final int AUTO_JUMP_TO_START = 1;
        public static final int AUTO_JUMP_TO_END = 2;
        public static final int AUTO_ANIMATE_TO_START = 3;
        public static final int AUTO_ANIMATE_TO_END = 4;
        private boolean mDisable = false;
        private int mPathMotionArc = UNSET;
        private int mLayoutDuringTransition = 0;
        private int mTransitionFlags = 0;
        static final int TRANSITION_FLAG_FIRST_DRAW = 1;
        static final int TRANSITION_FLAG_INTRA_AUTO = 2;
        static final int TRANSITION_FLAG_INTERCEPT_TOUCH = 4;

        public static final int INTERPOLATE_REFERENCE_ID = -2;
        public static final int INTERPOLATE_SPLINE_STRING = -1;
        public static final int INTERPOLATE_EASE_IN_OUT = 0;
        public static final int INTERPOLATE_EASE_IN = 1;
        public static final int INTERPOLATE_EASE_OUT = 2;
        public static final int INTERPOLATE_LINEAR = 3;
        public static final int INTERPOLATE_BOUNCE = 4;
        public static final int INTERPOLATE_OVERSHOOT = 5;
        public static final int INTERPOLATE_ANTICIPATE = 6;

        /**
         * Set the onSwipe for this Transition
         * @param onSwipe
         */
        public void setOnSwipe(OnSwipe onSwipe) {
            mTouchResponse = (onSwipe == null) ? null
                    : new TouchResponse(mMotionScene.mMotionLayout, onSwipe);
        }

        /**
         * Add the onclick to this view
         * @param id
         * @param action
         */
        public void addOnClick(int id, int action) {
            for (TransitionOnClick onClick : mOnClicks) {
                if (onClick.mTargetId == id) {
                    onClick.mMode = action;
                    return;
                }
            }
            TransitionOnClick click = new TransitionOnClick(this, id, action);
            mOnClicks.add(click);
        }

        /**
         * Remove the onclick added to this view
         * @param id
         */
        public void removeOnClick(int id) {
            TransitionOnClick toRemove = null;
            for (TransitionOnClick onClick : mOnClicks) {
                if (onClick.mTargetId == id) {
                    toRemove = onClick;
                    break;
                }
            }
            if (toRemove != null) {
                mOnClicks.remove(toRemove);
            }
        }

        /**
         * get the mode of layout during transition
         * @return
         */
        public int getLayoutDuringTransition() {
            return mLayoutDuringTransition;
        }

        /**
         * set the mode of layout during transition
         * @param mode
         */
        public void setLayoutDuringTransition(int mode) {
            mLayoutDuringTransition = mode;
        }

        /**
         * Add on Click support using the xml parser
         * @param context
         * @param parser
         */
        public void addOnClick(Context context, XmlPullParser parser) {
            mOnClicks.add(new TransitionOnClick(context, this, parser));
        }

        /**
         * sets the autoTransitionType
         * On reaching a state auto transitions may be run based on
         * one of AUTO_NONE, AUTO_JUMP_TO_START, AUTO_JUMP_TO_END, AUTO_ANIMATE_TO_START,
         * AUTO_ANIMATE_TO_END
         *
         * @param type
         */
        public void setAutoTransition(int type) {
            mAutoTransition = type;
        }

        /**
         * return the autoTransitionType.
         * one of AUTO_NONE, AUTO_JUMP_TO_START, AUTO_JUMP_TO_END, AUTO_ANIMATE_TO_START,
         * AUTO_ANIMATE_TO_END
         *
         * @return 0=NONE, 1=JUMP_TO_START, 2=JUMP_TO_END, 3=ANIMATE_TO_START, 4=ANIMATE_TO_END
         */
        public int getAutoTransition() {
            return mAutoTransition;
        }

        /**
         * Transitions can be given and ID. If unset it returns UNSET (-1)
         *
         * @return The Id of the Transition set in the MotionScene File or UNSET (-1)
         */
        public int getId() {
            return mId;
        }

        /**
         * Get the id of the constraint set to go to
         *
         * @return
         */
        public int getEndConstraintSetId() {
            return mConstraintSetEnd;
        }

        /**
         * Gets the id of the starting constraint set
         *
         * @return
         */
        public int getStartConstraintSetId() {
            return mConstraintSetStart;
        }

        /**
         * sets the duration of the transition
         * if set to < 8 it will be set to 8
         *
         * @param duration in milliseconds (min is 8)
         */
        public void setDuration(int duration) {
            this.mDuration = Math.max(duration, MIN_DURATION);
        }

        /**
         * gets the default transition duration
         *
         * @return duration int milliseconds
         */
        public int getDuration() {
            return mDuration;
        }

        /**
         * Gets the stagger value.
         *
         * @return
         */
        public float getStagger() {
            return mStagger;
        }

        public List<KeyFrames> getKeyFrameList() {
            return mKeyFramesList;
        }

        /**
         *  add a keyframe to this motion scene
         * @param keyFrames
         */
        public void addKeyFrame(KeyFrames keyFrames) {
            mKeyFramesList.add(keyFrames);
        }

        /**
         * Get the onClick handlers.
         *
         * @return list of on click handler
         */
        public List<TransitionOnClick> getOnClickList() {
            return mOnClicks;
        }

        /**
         * Get the Touch response manager
         *
         * @return
         */
        @SuppressWarnings("HiddenTypeParameter")
        public TouchResponse getTouchResponse() {
            return mTouchResponse;
        }

        /**
         * Sets the stagger value.
         * A Stagger value of zero means no stagger.
         * A Stagger value of 1 means the last view starts moving at .5 progress
         *
         * @param stagger
         */
        public void setStagger(float stagger) {
            mStagger = stagger;
        }

        /**
         * Sets the pathMotionArc for the all motions in this transition.
         * if set to UNSET (default) it reverts to the setting of the constraintSet
         *
         * @param arcMode
         */
        public void setPathMotionArc(int arcMode) {
            mPathMotionArc = arcMode;
        }

        /**
         * gets the pathMotionArc for the all motions in this transition.
         * if set to UNSET (default) it reverts to the setting of the constraintSet
         *
         * @return arcMode
         */
        public int getPathMotionArc() {
            return mPathMotionArc;
        }

        /**
         * Returns true if this Transition can be auto considered for transition
         * Default is enabled
         */
        public boolean isEnabled() {
            return !mDisable;
        }

        /**
         * enable or disable the Transition. If a Transition is disabled it is not eligible
         * for automatically switching to.
         *
         * @param enable
         */
        public void setEnabled(boolean enable) {
            mDisable = !enable;
        }

        /**
         * Print a debug string indicating the starting and ending state of the transition
         *
         * @param context
         * @return
         */
        public String debugString(Context context) {

            String ret;
            if (mConstraintSetStart == UNSET) {
                ret = "null";
            } else {
                ret = context.getResources().getResourceEntryName(mConstraintSetStart);
            }
            if (mConstraintSetEnd == UNSET) {
                ret += " -> " + "null";
            } else {
                ret += " -> " + context.getResources().getResourceEntryName(mConstraintSetEnd);
            }
            return ret;
        }

        /**
         * is the transition flag set
         * @param flag
         * @return
         */
        public boolean isTransitionFlag(int flag) {
            return 0 != (mTransitionFlags & flag);
        }

        public void setTransitionFlag(int flag) {
            mTransitionFlags = flag;
        }

        /**
         * Set the on touch up mode
         * @param touchUpMode
         */
        public void setOnTouchUp(int touchUpMode) {
            TouchResponse touchResponse = getTouchResponse();
            if (touchResponse != null) {
                touchResponse.setTouchUpMode(touchUpMode);
            }
        }

        public static class TransitionOnClick implements View.OnClickListener {
            public static final int ANIM_TO_END = 0x0001;
            public static final int ANIM_TOGGLE = 0x0011;
            public static final int ANIM_TO_START = 0x0010;
            public static final int JUMP_TO_END = 0x100;
            public static final int JUMP_TO_START = 0x1000;
            private final Transition mTransition;
            int mTargetId = UNSET;
            int mMode = 0x11;

            public TransitionOnClick(Context context,
                                     Transition transition,
                                     XmlPullParser parser) {
                mTransition = transition;
                TypedArray a = context.obtainStyledAttributes(Xml.asAttributeSet(parser),
                        R.styleable.OnClick);
                final int count = a.getIndexCount();
                for (int i = 0; i < count; i++) {
                    int attr = a.getIndex(i);
                    if (attr == R.styleable.OnClick_targetId) {
                        mTargetId = a.getResourceId(attr, mTargetId);
                    } else if (attr == R.styleable.OnClick_clickAction) {
                        mMode = a.getInt(attr, mMode);
                    }
                }
                a.recycle();
            }

            public TransitionOnClick(Transition transition, int id, int action) {
                mTransition = transition;
                mTargetId = id;
                mMode = action;
            }

            /**
             * Add the on click listeners for the current state
             *
             * @param motionLayout
             * @param currentState
             * @param transition
             */
            public void addOnClickListeners(MotionLayout motionLayout,
                                            int currentState,
                                            Transition transition) {
                View v = mTargetId == UNSET ? motionLayout : motionLayout.findViewById(mTargetId);
                if (v == null) {
                    Log.e(TAG, "OnClick could not find id " + mTargetId);
                    return;
                }
                int start = transition.mConstraintSetStart;
                int end = transition.mConstraintSetEnd;
                if (start == UNSET) { // does not require a known end state
                    v.setOnClickListener(this);
                    return;
                }

                boolean listen = ((mMode & ANIM_TO_END) != 0) && currentState == start;
                listen |= ((mMode & JUMP_TO_END) != 0) && currentState == start;
                listen |= ((mMode & ANIM_TO_END) != 0) && currentState == start;
                listen |= ((mMode & ANIM_TO_START) != 0) && currentState == end;
                listen |= ((mMode & JUMP_TO_START) != 0) && currentState == end;

                if (listen) {
                    v.setOnClickListener(this);
                }
            }

            /**
             * Remove the OnClickListeners
             * (typically called because you are removing the transition)
             *
             * @param motionLayout
             */
            public void removeOnClickListeners(MotionLayout motionLayout) {
                if (mTargetId == UNSET) {
                    return;
                }
                View v = motionLayout.findViewById(mTargetId);
                if (v == null) {
                    Log.e(TAG, " (*)  could not find id " + mTargetId);
                    return;
                }
                v.setOnClickListener(null);
            }

            boolean isTransitionViable(Transition current, MotionLayout tl) {
                if (mTransition == current) {
                    return true;
                }
                int dest = mTransition.mConstraintSetEnd;
                int from = mTransition.mConstraintSetStart;
                if (from == UNSET) {
                    return tl.mCurrentState != dest;
                }
                return (tl.mCurrentState == from) || (tl.mCurrentState == dest);

            }

            @Override
            public void onClick(View view) {
                MotionLayout tl = mTransition.mMotionScene.mMotionLayout;
                if (!tl.isInteractionEnabled()) {
                    return;
                }
                if (mTransition.mConstraintSetStart == UNSET) {
                    int currentState = tl.getCurrentState();
                    if (currentState == UNSET) {
                        tl.transitionToState(mTransition.mConstraintSetEnd);
                        return;
                    }
                    Transition t = new Transition(mTransition.mMotionScene, mTransition);
                    t.mConstraintSetStart = currentState;
                    t.mConstraintSetEnd = mTransition.mConstraintSetEnd;
                    tl.setTransition(t);
                    tl.transitionToEnd();
                    return;
                }
                Transition current = mTransition.mMotionScene.mCurrentTransition;
                boolean forward = ((mMode & ANIM_TO_END) != 0 || (mMode & JUMP_TO_END) != 0);
                boolean backward = ((mMode & ANIM_TO_START) != 0 || (mMode & JUMP_TO_START) != 0);
                boolean bidirectional = forward && backward;
                if (bidirectional) {
                    if (mTransition.mMotionScene.mCurrentTransition != mTransition) {
                        tl.setTransition(mTransition);
                    }
                    if (tl.getCurrentState() == tl.getEndState() || tl.getProgress() > 0.5f) {
                        forward = false;
                    } else {
                        backward = false;
                    }
                }
                if (isTransitionViable(current, tl)) {
                    if (forward && (mMode & ANIM_TO_END) != 0) {
                        tl.setTransition(mTransition);
                        tl.transitionToEnd();
                    } else if (backward && (mMode & ANIM_TO_START) != 0) {
                        tl.setTransition(mTransition);
                        tl.transitionToStart();
                    } else if (forward && (mMode & JUMP_TO_END) != 0) {
                        tl.setTransition(mTransition);
                        tl.setProgress(1);
                    } else if (backward && (mMode & JUMP_TO_START) != 0) {
                        tl.setTransition(mTransition);
                        tl.setProgress(0);
                    }
                }
            }
        }

        Transition(MotionScene motionScene, Transition global) {
            mMotionScene = motionScene;
            mDuration = motionScene.mDefaultDuration;
            if (global != null) {
                mPathMotionArc = global.mPathMotionArc;
                mDefaultInterpolator = global.mDefaultInterpolator;
                mDefaultInterpolatorString = global.mDefaultInterpolatorString;
                mDefaultInterpolatorID = global.mDefaultInterpolatorID;
                mDuration = global.mDuration;
                mKeyFramesList = global.mKeyFramesList;
                mStagger = global.mStagger;
                mLayoutDuringTransition = global.mLayoutDuringTransition;
            }
        }

        /**
         * Create a transition
         *
         * @param id                   a unique id to represent the transition.
         * @param motionScene          the motion scene that the transition will be added to.
         * @param constraintSetStartId id of the ConstraintSet to be used for the start of
         *                             transition
         * @param constraintSetEndId   id of the ConstraintSet to be used for the end of transition
         */
        public Transition(
                int id,
                MotionScene motionScene,
                int constraintSetStartId,
                int constraintSetEndId) {
            mId = id;
            mMotionScene = motionScene;
            mConstraintSetStart = constraintSetStartId;
            mConstraintSetEnd = constraintSetEndId;
            mDuration = motionScene.mDefaultDuration;
            mLayoutDuringTransition = motionScene.mLayoutDuringTransition;
        }

        Transition(MotionScene motionScene, Context context, XmlPullParser parser) {
            mDuration = motionScene.mDefaultDuration;
            mLayoutDuringTransition = motionScene.mLayoutDuringTransition;
            mMotionScene = motionScene;
            fillFromAttributeList(motionScene, context, Xml.asAttributeSet(parser));
        }

        /**
         * Sets the interpolation used for this transition.
         * <br>
         * The call support standard types EASE_IN_OUT etc.:<br>
         * setInterpolatorInfo(MotionScene.Transition.INTERPOLATE_EASE_IN_OUT, null, 0);
         * setInterpolatorInfo(MotionScene.Transition.INTERPOLATE_OVERSHOOT, null, 0);
         * <br>
         * Strings such as "cubic(...)" , "spline(...)"<br>
         * setInterpolatorInfo(
         * MotionScene.Transition.INTERPOLATE_SPLINE_STRING, "cubic(1,0,0,1)", 0);
         * <br>
         * Android interpolators in res/anim : <br>
         * setInterpolatorInfo(
         * MotionScene.Transition.INTERPOLATE_REFERENCE_ID, null, R.anim....);
         * <br>
         * @param interpolator sets the type of interpolation (MotionScene.Transition.INTERPOLATE_*)
         * @param interpolatorString sets a string based custom interpolation
         * @param interpolatorID sets the id of a Android Transition
         */
        public void setInterpolatorInfo(int interpolator,
                                        String interpolatorString,
                                        int interpolatorID) {
            mDefaultInterpolator = interpolator;
            mDefaultInterpolatorString = interpolatorString;
            mDefaultInterpolatorID = interpolatorID;
        }

        private void fillFromAttributeList(MotionScene motionScene,
                                           Context context,
                                           AttributeSet attrs) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Transition);
            fill(motionScene, context, a);
            a.recycle();
        }

        private void fill(MotionScene motionScene, Context context, TypedArray a) {
            final int count = a.getIndexCount();
            for (int i = 0; i < count; i++) {
                int attr = a.getIndex(i);
                if (attr == R.styleable.Transition_constraintSetEnd) {
                    mConstraintSetEnd = a.getResourceId(attr, UNSET);
                    String type = context.getResources().getResourceTypeName(mConstraintSetEnd);
                    if ("layout".equals(type)) {
                        ConstraintSet cSet = new ConstraintSet();
                        cSet.load(context, mConstraintSetEnd);
                        motionScene.mConstraintSetMap.append(mConstraintSetEnd, cSet);
                        if (DEBUG) {
                            Log.v(TAG, " constraint Set end loaded from layout "
                                    + Debug.getName(context, mConstraintSetEnd));
                        }
                    } else if ("xml".equals(type)) {
                        int id = motionScene.parseInclude(context, mConstraintSetEnd);
                        mConstraintSetEnd = id;
                    }
                } else if (attr == R.styleable.Transition_constraintSetStart) {
                    mConstraintSetStart = a.getResourceId(attr, mConstraintSetStart);
                    String type = context.getResources().getResourceTypeName(mConstraintSetStart);
                    if ("layout".equals(type)) {
                        ConstraintSet cSet = new ConstraintSet();
                        cSet.load(context, mConstraintSetStart);
                        motionScene.mConstraintSetMap.append(mConstraintSetStart, cSet);
                    } else if ("xml".equals(type)) {
                        int id = motionScene.parseInclude(context, mConstraintSetStart);
                        mConstraintSetStart = id;
                    }
                } else if (attr == R.styleable.Transition_motionInterpolator) {
                    TypedValue type = a.peekValue(attr);

                    if (type.type == TypedValue.TYPE_REFERENCE) {
                        mDefaultInterpolatorID = a.getResourceId(attr, -1);
                        if (mDefaultInterpolatorID != -1) {
                            mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID;
                        }
                    } else if (type.type == TypedValue.TYPE_STRING) {
                        mDefaultInterpolatorString = a.getString(attr);
                        if (mDefaultInterpolatorString != null) {
                            if (mDefaultInterpolatorString.indexOf("/") > 0) {
                                mDefaultInterpolatorID = a.getResourceId(attr, -1);
                                mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID;
                            } else {
                                mDefaultInterpolator = SPLINE_STRING;
                            }
                        }
                    } else {
                        mDefaultInterpolator = a.getInteger(attr, mDefaultInterpolator);
                    }

                } else if (attr == R.styleable.Transition_duration) {
                    mDuration = a.getInt(attr, mDuration);
                    if (mDuration < MIN_DURATION) {
                        mDuration = MIN_DURATION;
                    }
                } else if (attr == R.styleable.Transition_staggered) {
                    mStagger = a.getFloat(attr, mStagger);
                } else if (attr == R.styleable.Transition_autoTransition) {
                    mAutoTransition = a.getInteger(attr, mAutoTransition);
                } else if (attr == R.styleable.Transition_android_id) {
                    mId = a.getResourceId(attr, mId);
                } else if (attr == R.styleable.Transition_transitionDisable) {
                    mDisable = a.getBoolean(attr, mDisable);
                } else if (attr == R.styleable.Transition_pathMotionArc) {
                    mPathMotionArc = a.getInteger(attr, UNSET);
                } else if (attr == R.styleable.Transition_layoutDuringTransition) {
                    mLayoutDuringTransition = a.getInteger(attr, 0);
                } else if (attr == R.styleable.Transition_transitionFlags) {
                    mTransitionFlags = a.getInteger(attr, 0);
                }
            }
            if (mConstraintSetStart == UNSET) {
                mIsAbstract = true;
            }
        }

    }

    /**
     * Create a motion scene.
     *
     * @param layout Motion layout to which the scene will be set.
     */
    public MotionScene(MotionLayout layout) {
        mMotionLayout = layout;
        mViewTransitionController = new ViewTransitionController(layout);
    }

    MotionScene(Context context, MotionLayout layout, int resourceID) {
        mMotionLayout = layout;
        mViewTransitionController = new ViewTransitionController(layout);

        load(context, resourceID);
        mConstraintSetMap.put(R.id.motion_base, new ConstraintSet());
        mConstraintSetIdMap.put("motion_base", R.id.motion_base);
    }

    /**
     * Load a MotionScene   from a MotionScene.xml file
     *
     * @param context    the context for the inflation
     * @param resourceId id of xml file in res/xml/
     */
    private void load(Context context, int resourceId) {
        Resources res = context.getResources();
        XmlPullParser parser = res.getXml(resourceId);
        try {
            Transition transition = null;
            for (int eventType = parser.getEventType();
                    eventType != XmlResourceParser.END_DOCUMENT;
                    eventType = parser.next()) {
                switch (eventType) {
                    case XmlResourceParser.START_DOCUMENT:
                    case XmlResourceParser.END_TAG:
                    case XmlResourceParser.TEXT:
                        break;
                    case XmlResourceParser.START_TAG:
                        String tagName = parser.getName();
                        if (DEBUG_DESKTOP) {
                            System.out.println("parsing = " + tagName);
                        }
                        if (DEBUG) {
                            Log.v(TAG, "MotionScene ----------- START_TAG " + tagName);
                        }
                        switch (tagName) {
                            case MOTIONSCENE_TAG:
                                parseMotionSceneTags(context, parser);
                                break;
                            case TRANSITION_TAG:
                                mTransitionList.add(transition =
                                        new Transition(this, context, parser));
                                if (mCurrentTransition == null && !transition.mIsAbstract) {
                                    mCurrentTransition = transition;
                                    if (mCurrentTransition != null
                                            && mCurrentTransition.mTouchResponse != null) {
                                        mCurrentTransition.mTouchResponse.setRTL(mRtl);
                                    }
                                }
                                if (transition.mIsAbstract) { // global transition only one for now
                                    if (transition.mConstraintSetEnd == UNSET) {
                                        mDefaultTransition = transition;
                                    } else {
                                        mAbstractTransitionList.add(transition);
                                    }
                                    mTransitionList.remove(transition);
                                }
                                break;
                            case ONSWIPE_TAG:
                                if (DEBUG || transition == null) {
                                    String name = context.getResources()
                                            .getResourceEntryName(resourceId);
                                    int line = parser.getLineNumber();
                                    Log.v(TAG, " OnSwipe (" + name + ".xml:" + line + ")");
                                }
                                if (transition != null) {
                                    transition.mTouchResponse =
                                            new TouchResponse(context, mMotionLayout, parser);
                                }
                                break;
                            case ONCLICK_TAG:
                                if (transition != null) {
                                    if (!mMotionLayout.isInEditMode()) {
                                        transition.addOnClick(context, parser);
                                    }
                                }
                                break;
                            case STATESET_TAG:
                                mStateSet = new StateSet(context, parser);
                                break;
                            case CONSTRAINTSET_TAG:
                                parseConstraintSet(context, parser);
                                break;
                            case INCLUDE_TAG:
                            case INCLUDE_TAG_UC:
                                parseInclude(context, parser);
                                break;
                            case KEYFRAMESET_TAG:
                                KeyFrames keyFrames = new KeyFrames(context, parser);
                                if (transition != null) {
                                    transition.mKeyFramesList.add(keyFrames);
                                }
                                break;
                            case VIEW_TRANSITION:
                                ViewTransition viewTransition = new ViewTransition(context, parser);
                                mViewTransitionController.add(viewTransition);
                                break;
                            default:
                                if (DEBUG) {
                                    Log.v(TAG, getLine(context, resourceId, parser)
                                            + "WARNING UNKNOWN ATTRIBUTE " + tagName);
                                }
                                break;
                        }

                        break;
                }
            }
        } catch (XmlPullParserException e) {
            if (DEBUG) {
                Log.v(TAG, getLine(context, resourceId, parser) + " " + e.getMessage());
            }
            Log.e(TAG, "Error parsing resource: " + resourceId, e);
        } catch (IOException e) {
            if (DEBUG) {
                Log.v(TAG, getLine(context, resourceId, parser) + " " + e.getMessage());
            }
            Log.e(TAG, "Error parsing resource: " + resourceId, e);
        }
    }

    private void parseMotionSceneTags(Context context, XmlPullParser parser) {
        AttributeSet attrs = Xml.asAttributeSet(parser);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MotionScene);
        final int count = a.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = a.getIndex(i);
            if (attr == R.styleable.MotionScene_defaultDuration) {
                mDefaultDuration = a.getInt(attr, mDefaultDuration);
                if (mDefaultDuration < MIN_DURATION) {
                    mDefaultDuration = MIN_DURATION;
                }
            } else if (attr == R.styleable.MotionScene_layoutDuringTransition) {
                mLayoutDuringTransition = a.getInteger(attr, LAYOUT_IGNORE_REQUEST);
            }
        }
        a.recycle();
    }

    private int getId(Context context, String idString) {
        int id = UNSET;
        if (idString.contains("/")) {
            String tmp = idString.substring(idString.indexOf('/') + 1);
            id = context.getResources().getIdentifier(tmp, "id", context.getPackageName());
            if (DEBUG_DESKTOP) {
                System.out.println("id getMap res = " + id);
            }
        }
        if (id == UNSET) {
            if (idString != null && idString.length() > 1) {
                id = Integer.parseInt(idString.substring(1));
            } else {
                Log.e(TAG, "error in parsing id");
            }
        }
        return id;
    }

    private void parseInclude(Context context, XmlPullParser mainParser) {
        TypedArray a = context
                .obtainStyledAttributes(Xml.asAttributeSet(mainParser), R.styleable.include);
        final int count = a.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = a.getIndex(i);
            if (attr == R.styleable.include_constraintSet) {
                int resourceId = a.getResourceId(attr, UNSET);
                parseInclude(context, resourceId);
            }
        }
        a.recycle();
    }

    private int parseInclude(Context context, int resourceId) {
        Resources res = context.getResources();
        XmlPullParser includeParser = res.getXml(resourceId);
        try {
            for (int eventType = includeParser.getEventType();
                    eventType != XmlResourceParser.END_DOCUMENT;
                    eventType = includeParser.next()) {
                String tagName = includeParser.getName();
                if (XmlResourceParser.START_TAG == eventType
                        && CONSTRAINTSET_TAG.equals(tagName)) {
                    return parseConstraintSet(context, includeParser);
                }
            }
        } catch (XmlPullParserException e) {
            if (DEBUG) {
                Log.v(TAG, getLine(context, resourceId, includeParser)
                        + " " + e.getMessage());
            }
            Log.e(TAG, "Error parsing resource: " + resourceId, e);
        } catch (IOException e) {
            if (DEBUG) {
                Log.v(TAG, getLine(context, resourceId, includeParser)
                        + " " + e.getMessage());
            }
            Log.e(TAG, "Error parsing resource: " + resourceId, e);
        }
        return UNSET;
    }

    private int parseConstraintSet(Context context, XmlPullParser parser) {
        ConstraintSet set = new ConstraintSet();
        set.setForceId(false);
        int count = parser.getAttributeCount();
        int id = UNSET;
        int derivedId = UNSET;
        for (int i = 0; i < count; i++) {
            String name = parser.getAttributeName(i);
            String value = parser.getAttributeValue(i);
            if (DEBUG_DESKTOP) {
                System.out.println("id string = " + value);
            }
            switch (name) {
                case "id":
                    id = getId(context, value);
                    mConstraintSetIdMap.put(stripID(value), id);
                    set.mIdString = Debug.getName(context, id);
                    break;
                case "deriveConstraintsFrom":
                    derivedId = getId(context, value);
                    break;
                case "stateLabels":
                    set.setStateLabels(value);
                    break;
                case "constraintRotate":
                    try {
                        set.mRotate = Integer.parseInt(value);
                    } catch (NumberFormatException exception) {
                        switch (value) {
                            case "none":
                                set.mRotate = 0;
                                break;
                            case "right":
                                set.mRotate = 1;
                                break;
                            case "left":
                                set.mRotate = 2;
                                break;
                            case "x_right":
                                set.mRotate = 3;
                                break;
                            case "x_left":
                                set.mRotate = 4;
                                break;
                        }
                    }


                    break;
            }
        }
        if (id != UNSET) {
            if (mMotionLayout.mDebugPath != 0) {
                set.setValidateOnParse(true);
            }
            set.load(context, parser);
            if (derivedId != UNSET) {
                mDeriveMap.put(id, derivedId);
            }
            mConstraintSetMap.put(id, set);
        }
        return id;
    }

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

    /**
     * Get the constraintSet given the id
     * @param context
     * @param id
     * @return
     */
    public ConstraintSet getConstraintSet(Context context, String id) {
        if (DEBUG_DESKTOP) {
            System.out.println("id " + id);
            System.out.println("size " + mConstraintSetMap.size());
        }
        for (int i = 0; i < mConstraintSetMap.size(); i++) {
            int key = mConstraintSetMap.keyAt(i);
            String IdAsString = context.getResources().getResourceName(key);
            if (DEBUG_DESKTOP) {
                System.out.println("Id for <" + i + "> is <"
                        + IdAsString + "> looking for <" + id + ">");
            }
            if (id.equals(IdAsString)) {
                return mConstraintSetMap.get(key);
            }
        }
        return null;
    }

    ConstraintSet getConstraintSet(int id) {
        return getConstraintSet(id, -1, -1);
    }

    ConstraintSet getConstraintSet(int id, int width, int height) {
        if (DEBUG_DESKTOP) {
            System.out.println("id " + id);
            System.out.println("size " + mConstraintSetMap.size());
        }
        if (mStateSet != null) {
            int cid = mStateSet.stateGetConstraintID(id, width, height);
            if (cid != -1) {
                id = cid;
            }
        }
        if (mConstraintSetMap.get(id) == null) {
            Log.e(TAG, "Warning could not find ConstraintSet id/"
                    + Debug.getName(mMotionLayout.getContext(), id) + " In MotionScene");
            return mConstraintSetMap.get(mConstraintSetMap.keyAt(0));
        }
        return mConstraintSetMap.get(id);
    }

    /**
     * Maps the Constraint set to the id.
     *
     * @param id  - unique id to represent the ConstraintSet
     * @param set - ConstraintSet to be represented with the id.
     */
    public void setConstraintSet(int id, ConstraintSet set) {
        mConstraintSetMap.put(id, set);
    }

    /**
     * provides the key frames & CycleFrames to the motion view to
     *
     * @param motionController
     */
    public void getKeyFrames(MotionController motionController) {
        if (mCurrentTransition == null) {
            if (mDefaultTransition != null) {
                for (KeyFrames keyFrames : mDefaultTransition.mKeyFramesList) {
                    keyFrames.addFrames(motionController);
                }
            }
            return;
        }
        for (KeyFrames keyFrames : mCurrentTransition.mKeyFramesList) {
            keyFrames.addFrames(motionController);
        }
    }

    /**
     * get key frame
     *
     * @param context
     * @param type
     * @param target
     * @param position
     * @return Key Object
     */
    Key getKeyFrame(Context context, int type, int target, int position) {
        if (mCurrentTransition == null) {
            return null;
        }
        for (KeyFrames keyFrames : mCurrentTransition.mKeyFramesList) {
            for (Integer integer : keyFrames.getKeys()) {
                if (target == integer) {
                    ArrayList<Key> keys = keyFrames.getKeyFramesForView(integer);
                    for (Key key : keys) {
                        if (key.mFramePosition == position) {
                            if (key.mType == type) {
                                return key;
                            }
                        }
                    }
                }
            }

        }
        return null;
    }

    int getTransitionDirection(int stateId) {
        for (Transition transition : mTransitionList) {
            if (transition.mConstraintSetStart == stateId) {
                return TRANSITION_BACKWARD;
            }
        }
        return TRANSITION_FORWARD;
    }

    /**
     * Returns true if the view has a keyframe defined at the given position
     *
     * @param view
     * @param position
     * @return true if a keyframe exists, false otherwise
     */
    boolean hasKeyFramePosition(View view, int position) {
        if (mCurrentTransition == null) {
            return false;
        }
        for (KeyFrames keyFrames : mCurrentTransition.mKeyFramesList) {
            ArrayList<Key> framePoints = keyFrames.getKeyFramesForView(view.getId());
            for (Key framePoint : framePoints) {
                if (framePoint.mFramePosition == position) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Set a keyFrame on the current Transition
     * @param view
     * @param position
     * @param name
     * @param value
     */
    public void setKeyframe(View view, int position, String name, Object value) {
        if (DEBUG) {
            System.out.println("setKeyframe for pos " + position
                    + " name <" + name + "> value: " + value);
        }
        if (mCurrentTransition == null) {
            return;
        }
        for (KeyFrames keyFrames : mCurrentTransition.mKeyFramesList) {
            if (DEBUG) {
                System.out.println("key frame " + keyFrames);
            }
            ArrayList<Key> framePoints = keyFrames.getKeyFramesForView(view.getId());
            if (DEBUG) {
                System.out.println("key frame has " + framePoints.size() + " frame points");
            }
            for (Key framePoint : framePoints) {
                if (DEBUG) {
                    System.out.println("framePoint pos: " + framePoint.mFramePosition);
                }
                if (framePoint.mFramePosition == position) {
                    float v = 0;
                    if (value != null) {
                        v = ((Float) value).floatValue();
                        if (DEBUG) {
                            System.out.println("value: " + v);
                        }
                    } else {
                        if (DEBUG) {
                            System.out.println("value was null!!!");
                        }
                    }
                    if (v == 0) {
                        v = 0.01f;
                    }
                }
            }
        }
    }

    /**
     * Get the path percent  (Non functional currently)
     * @param view
     * @param position
     * @return
     */
    public float getPathPercent(View view, int position) {
        return 0;
    }

    //////////////////////////////////////////////////////////
    // touch handling
    ///////////////////////////////////////////////////////////
    boolean supportTouch() {
        for (Transition transition : mTransitionList) {
            if (transition.mTouchResponse != null) {
                return true;
            }
        }
        return mCurrentTransition != null && mCurrentTransition.mTouchResponse != null;
    }

    float mLastTouchX, mLastTouchY;

    void processTouchEvent(MotionEvent event, int currentState, MotionLayout motionLayout) {
        if (DEBUG) {
            Log.v(TAG, Debug.getLocation() + " processTouchEvent");
        }
        RectF cache = new RectF();
        if (mVelocityTracker == null) {
            mVelocityTracker = mMotionLayout.obtainVelocityTracker();
        }
        mVelocityTracker.addMovement(event);
        if (DEBUG) {
            float time = (event.getEventTime() % 100000) / 1000f;
            float x = event.getRawX();
            float y = event.getRawY();
            Log.v(TAG, " " + time + "  processTouchEvent "
                    + "state=" + Debug.getState(motionLayout, currentState)
                    + "  " + Debug.getActionType(event) + " " + x
                    + ", " + y + " \t " + motionLayout.getProgress());
        }

        if (currentState != -1) {
            RectF region;
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mLastTouchX = event.getRawX();
                    mLastTouchY = event.getRawY();
                    mLastTouchDown = event;
                    mIgnoreTouch = false;
                    if (mCurrentTransition.mTouchResponse != null) {
                        region = mCurrentTransition.mTouchResponse
                                .getLimitBoundsTo(mMotionLayout, cache);
                        if (region != null
                                && !region.contains(mLastTouchDown.getX(), mLastTouchDown.getY())) {
                            mLastTouchDown = null;
                            mIgnoreTouch = true;
                            return;
                        }
                        region = mCurrentTransition.mTouchResponse
                                .getTouchRegion(mMotionLayout, cache);
                        if (region != null
                                && !region.contains(mLastTouchDown.getX(),
                                mLastTouchDown.getY())) {
                            mMotionOutsideRegion = true;
                        } else {
                            mMotionOutsideRegion = false;
                        }
                        mCurrentTransition.mTouchResponse.setDown(mLastTouchX, mLastTouchY);
                    }
                    if (DEBUG) {
                        Log.v(TAG, "----- ACTION_DOWN " + mLastTouchX + "," + mLastTouchY);
                    }
                    return;
                case MotionEvent.ACTION_MOVE:
                    if (mIgnoreTouch) {
                        break;
                    }
                    float dy = event.getRawY() - mLastTouchY;
                    float dx = event.getRawX() - mLastTouchX;
                    if (DEBUG) {
                        Log.v(TAG, "----- ACTION_MOVE " + dx + "," + dy);
                    }
                    if ((dx == 0.0 && dy == 0.0) || mLastTouchDown == null) {
                        return;
                    }

                    Transition transition =
                            bestTransitionFor(currentState, dx, dy, mLastTouchDown);
                    if (DEBUG) {
                        Log.v(TAG, Debug.getLocation() + " best Transition For "
                                + dx + "," + dy + " "
                                + ((transition == null) ? null
                                        : transition.debugString(mMotionLayout.getContext())));
                    }
                    if (transition != null) {

                        motionLayout.setTransition(transition);
                        region = mCurrentTransition
                                .mTouchResponse.getTouchRegion(mMotionLayout, cache);
                        mMotionOutsideRegion = region != null
                                && !region.contains(mLastTouchDown.getX(), mLastTouchDown.getY());
                        mCurrentTransition.mTouchResponse.setUpTouchEvent(mLastTouchX, mLastTouchY);
                    }
            }
        }
        if (mIgnoreTouch) {
            return;
        }
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null
                && !mMotionOutsideRegion) {
            mCurrentTransition.mTouchResponse.processTouchEvent(event,
                    mVelocityTracker, currentState, this);
        }

        mLastTouchX = event.getRawX();
        mLastTouchY = event.getRawY();

        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
                if (motionLayout.mCurrentState != UNSET) {
                    autoTransition(motionLayout, motionLayout.mCurrentState);
                }
            }
        }
    }

    void processScrollMove(float dx, float dy) {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            mCurrentTransition.mTouchResponse.scrollMove(dx, dy);
        }
    }

    void processScrollUp(float dx, float dy) {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            mCurrentTransition.mTouchResponse.scrollUp(dx, dy);
        }
    }

    /**
     * Calculate if a drag in this direction results in an increase or decrease in progress.
     *
     * @param dx drag direction in x
     * @param dy drag direction in y
     * @return change in progress given that dx and dy
     */
    float getProgressDirection(float dx, float dy) {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getProgressDirection(dx, dy);
        }
        return 0;
    }

    /////////////////////////////////////////////////////////////

    int getStartId() {
        if (mCurrentTransition == null) {
            return -1;
        }
        return mCurrentTransition.mConstraintSetStart;
    }

    int getEndId() {
        if (mCurrentTransition == null) {
            return -1;
        }
        return mCurrentTransition.mConstraintSetEnd;
    }

    static final int EASE_IN_OUT = 0;
    static final int EASE_IN = 1;
    static final int EASE_OUT = 2;
    static final int LINEAR = 3;
    static final int BOUNCE = 4;
    static final int OVERSHOOT = 5;
    static final int ANTICIPATE = 6;

    /**
     * Get the interpolator define for the current Transition
     * @return
     */
    public Interpolator getInterpolator() {
        switch (mCurrentTransition.mDefaultInterpolator) {
            case SPLINE_STRING:
                final Easing easing = Easing
                        .getInterpolator(mCurrentTransition.mDefaultInterpolatorString);
                return new Interpolator() {
                    @Override
                    public float getInterpolation(float v) {
                        return (float) easing.get(v);
                    }
                };
            case INTERPOLATOR_REFERENCE_ID:
                return AnimationUtils.loadInterpolator(mMotionLayout.getContext(),
                        mCurrentTransition.mDefaultInterpolatorID);
            case EASE_IN_OUT:
                return new AccelerateDecelerateInterpolator();
            case EASE_IN:
                return new AccelerateInterpolator();
            case EASE_OUT:
                return new DecelerateInterpolator();
            case LINEAR:
                return null;
            case ANTICIPATE:
                return new AnticipateInterpolator();
            case OVERSHOOT:
                return new OvershootInterpolator();
            case BOUNCE:
                return new BounceInterpolator();
        }
        return null;
    }

    /**
     * Get Duration of the current transition.
     *
     * @return duration in milliseconds
     */
    public int getDuration() {
        if (mCurrentTransition != null) {
            return mCurrentTransition.mDuration;
        }
        return mDefaultDuration;
    }

    /**
     * Sets the duration of the current transition or the default if there is no current transition
     *
     * @param duration in milliseconds
     */
    public void setDuration(int duration) {
        if (mCurrentTransition != null) {
            mCurrentTransition.setDuration(duration);
        } else {
            mDefaultDuration = duration;
        }
    }

    /**
     * The transition arc path mode
     * @return
     */
    public int gatPathMotionArc() {
        return (mCurrentTransition != null) ? mCurrentTransition.mPathMotionArc : UNSET;
    }

    /**
     * Get the staggered value of the current transition.
     * Will default to 0 staggered if there is no current transition.
     *
     * @return
     */
    public float getStaggered() {
        if (mCurrentTransition != null) {
            return mCurrentTransition.mStagger;
        }
        return 0;
    }

    float getMaxAcceleration() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getMaxAcceleration();
        }
        return 0;
    }

    float getMaxVelocity() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getMaxVelocity();
        }
        return 0;
    }

    float getSpringStiffiness() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getSpringStiffness();
        }
        return 0;
    }

    float getSpringMass() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getSpringMass();
        }
        return 0;
    }

    float getSpringDamping() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getSpringDamping();
        }
        return 0;
    }

    float getSpringStopThreshold() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getSpringStopThreshold();
        }
        return 0;
    }
    int getSpringBoundary() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getSpringBoundary();
        }
        return 0;
    }
    int getAutoCompleteMode() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getAutoCompleteMode();
        }
        return 0;
    }
    void setupTouch() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            mCurrentTransition.mTouchResponse.setupTouch();
        }
    }

    boolean getMoveWhenScrollAtTop() {
        if (mCurrentTransition != null && mCurrentTransition.mTouchResponse != null) {
            return mCurrentTransition.mTouchResponse.getMoveWhenScrollAtTop();
        }
        return false;
    }

    /**
     * read the constraints from the inflation of the ConstraintLayout
     * If the constraintset does not contain information about a view this information is used
     * as a "fallback" position.
     *
     * @param motionLayout
     */
    void readFallback(MotionLayout motionLayout) {

        for (int i = 0; i < mConstraintSetMap.size(); i++) {
            int key = mConstraintSetMap.keyAt(i);
            if (hasCycleDependency(key)) {
                Log.e(TAG, "Cannot be derived from yourself");
                return;
            }
            readConstraintChain(key, motionLayout);
        }
    }

    /**
     * This is brute force but the number of ConstraintSets is typically very small (< 5)
     *
     * @param key
     * @return
     */
    private boolean hasCycleDependency(int key) {
        int derived = mDeriveMap.get(key);
        int len = mDeriveMap.size();
        while (derived > 0) {
            if (derived == key) {
                return true;
            }
            if (len-- < 0) {
                return true;
            }
            derived = mDeriveMap.get(derived);
        }
        return false;
    }

    /**
     * Recursive descent of the deriveConstraintsFrom tree reading the motionLayout if
     * needed.
     *
     * @param key
     */
    private void readConstraintChain(int key, MotionLayout motionLayout) {
        ConstraintSet cs = mConstraintSetMap.get(key);
        cs.derivedState = cs.mIdString;
        int derivedFromId = mDeriveMap.get(key);
        if (derivedFromId > 0) {
            readConstraintChain(derivedFromId, motionLayout);
            ConstraintSet derivedFrom = mConstraintSetMap.get(derivedFromId);
            if (derivedFrom == null) {
                Log.e(TAG, "ERROR! invalid deriveConstraintsFrom: @id/"
                        + Debug.getName(mMotionLayout.getContext(), derivedFromId));
                return;
            }
            cs.derivedState += "/" + derivedFrom.derivedState;
            cs.readFallback(derivedFrom);
        } else {
            cs.derivedState += "  layout";
            cs.readFallback(motionLayout);
        }
        cs.applyDeltaFrom(cs);
    }

    /**
     * Utility to strip the @id/ from an id
     * @param id
     * @return
     */
    public static String stripID(String id) {
        if (id == null) {
            return "";
        }
        int index = id.indexOf('/');
        if (index < 0) {
            return id;
        }
        return id.substring(index + 1);
    }

    /**
     * Used at design time
     *
     * @param id
     * @return
     */
    public int lookUpConstraintId(String id) {
        Integer boxed = mConstraintSetIdMap.get(id);
        if (boxed == null) {
            return 0;
        } else {
            return boxed;
        }
    }

    /**
     * used at design time
     *
     * @return
     */
    public String lookUpConstraintName(int id) {
        for (Map.Entry<String, Integer> entry : mConstraintSetIdMap.entrySet()) {
            Integer boxed = entry.getValue();
            if (boxed == null) {
                continue;
            }

            if (boxed == id) {
                return entry.getKey();
            }
        }
        return null;
    }

    /**
     * this allow disabling autoTransitions to prevent design surface from being in undefined states
     *
     * @param disable
     */
    public void disableAutoTransition(boolean disable) {
        mDisableAutoTransition = disable;
    }

    /**
     * Construct a user friendly error string
     *
     * @param context    the context
     * @param resourceId the xml being parsed
     * @param pullParser the XML parser
     * @return
     */
    static String getLine(Context context, int resourceId, XmlPullParser pullParser) {
        return ".(" + Debug.getName(context, resourceId) + ".xml:" + pullParser.getLineNumber()
                + ") \"" + pullParser.getName() + "\"";
    }
}