public class

MotionScene

extends java.lang.Object

 java.lang.Object

↳androidx.constraintlayout.motion.widget.MotionScene

Gradle dependencies

compile group: 'androidx.constraintlayout', name: 'constraintlayout', version: '2.2.0-alpha01'

  • groupId: androidx.constraintlayout
  • artifactId: constraintlayout
  • version: 2.2.0-alpha01

Artifact androidx.constraintlayout:constraintlayout:2.2.0-alpha01 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.constraintlayout:constraintlayout com.android.support.constraint:constraint-layout

Overview

The information to transition between multiple ConstraintSets This Class is meant to be used from XML

Summary

Fields
public static final intLAYOUT_CALL_MEASURE

public static final intLAYOUT_HONOR_REQUEST

public static final intLAYOUT_IGNORE_REQUEST

public static final intUNSET

Constructors
publicMotionScene(MotionLayout layout)

Create a motion scene.

Methods
public voidaddOnClickListeners(MotionLayout motionLayout, int currentState)

Add all on click listeners for the current state

public voidaddTransition(MotionScene.Transition transition)

Add a transition to the motion scene.

public booleanapplyViewTransition(int viewTransitionId, MotionController motionController)

Apply a view transition to the MotionController

public MotionScene.TransitionbestTransitionFor(int currentState, float dx, float dy, MotionEvent lastTouchDown)

Find the best transition for the motion

public voiddisableAutoTransition(boolean disable)

this allow disabling autoTransitions to prevent design surface from being in undefined states

public voidenableViewTransition(int id, boolean enable)

Enable this viewTransition

public intgatPathMotionArc()

The transition arc path mode

public ConstraintSetgetConstraintSet(Context context, java.lang.String id)

Get the constraintSet given the id

public int[]getConstraintSetIds()

Get the list of all Constraint Sets Know to the system

public java.util.ArrayList<MotionScene.Transition>getDefinedTransitions()

Get list of Transitions know to the system

public intgetDuration()

Get Duration of the current transition.

public InterpolatorgetInterpolator()

Get the interpolator define for the current Transition

public voidgetKeyFrames(MotionController motionController)

provides the key frames & CycleFrames to the motion view to

public int[]getMatchingStateLabels(java.lang.String types[])

Get the id's of all constraintSets with the matching types

public floatgetPathPercent(View view, int position)

Get the path percent (Non functional currently)

public floatgetStaggered()

Get the staggered value of the current transition.

public MotionScene.TransitiongetTransitionById(int id)

Find the transition based on the id

public java.util.List<MotionScene.Transition>getTransitionsWithState(int stateId)

Get all transitions that include this state

public booleanisViewTransitionEnabled(int id)

Is this view transition enabled

public intlookUpConstraintId(java.lang.String id)

Used at design time

public java.lang.StringlookUpConstraintName(int id)

used at design time

protected voidonLayout(boolean changed, int left, int top, int right, int bottom)

public voidremoveTransition(MotionScene.Transition transition)

Remove the transition with the matching id from the motion scene.

public voidsetConstraintSet(int id, ConstraintSet set)

Maps the Constraint set to the id.

public voidsetDuration(int duration)

Sets the duration of the current transition or the default if there is no current transition

public voidsetKeyframe(View view, int position, java.lang.String name, java.lang.Object value)

Set a keyFrame on the current Transition

public voidsetRtl(boolean rtl)

Set Right to left

public voidsetTransition(MotionScene.Transition transition)

Set the transition to be the current transition of the motion scene.

public static java.lang.StringstripID(java.lang.String id)

Utility to strip the @id/ from an id

public booleanvalidateLayout(MotionLayout layout)

public voidviewTransition(int id, View view[])

Apply the viewTransition on the list of views

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final int UNSET

public static final int LAYOUT_IGNORE_REQUEST

public static final int LAYOUT_HONOR_REQUEST

public static final int LAYOUT_CALL_MEASURE

Constructors

public MotionScene(MotionLayout layout)

Create a motion scene.

Parameters:

layout: Motion layout to which the scene will be set.

Methods

public void addTransition(MotionScene.Transition transition)

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.

public void removeTransition(MotionScene.Transition transition)

Remove the transition with the matching id from the motion scene. If no matching transition is found, it does nothing.

public boolean validateLayout(MotionLayout layout)

Returns:

true if the layout is valid for the scene. False otherwise. Use it for the debugging purposes.

public void setTransition(MotionScene.Transition transition)

Set the transition to be the current transition of the motion scene.

Parameters:

transition: a transition to be set. The transition must exist within the motion scene. (e.g. MotionScene.addTransition(MotionScene.Transition))

public java.util.List<MotionScene.Transition> getTransitionsWithState(int stateId)

Get all transitions that include this state

Parameters:

stateId:

Returns:

public void addOnClickListeners(MotionLayout motionLayout, int currentState)

Add all on click listeners for the current state

Parameters:

motionLayout:
currentState:

public MotionScene.Transition bestTransitionFor(int currentState, float dx, float dy, MotionEvent lastTouchDown)

Find the best transition for the motion

Parameters:

currentState:
dx: drag delta x
dy: drag delta y
lastTouchDown:

Returns:

public java.util.ArrayList<MotionScene.Transition> getDefinedTransitions()

Get list of Transitions know to the system

Returns:

public MotionScene.Transition getTransitionById(int id)

Find the transition based on the id

Parameters:

id:

Returns:

public int[] getConstraintSetIds()

Get the list of all Constraint Sets Know to the system

Returns:

public int[] getMatchingStateLabels(java.lang.String types[])

Get the id's of all constraintSets with the matching types

Returns:

public void setRtl(boolean rtl)

Set Right to left

Parameters:

rtl:

public void viewTransition(int id, View view[])

Apply the viewTransition on the list of views

Parameters:

id:
view:

public void enableViewTransition(int id, boolean enable)

Enable this viewTransition

Parameters:

id: of viewTransition
enable:

public boolean isViewTransitionEnabled(int id)

Is this view transition enabled

Parameters:

id: of viewTransition

Returns:

public boolean applyViewTransition(int viewTransitionId, MotionController motionController)

Apply a view transition to the MotionController

Parameters:

viewTransitionId: of viewTransition
motionController:

Returns:

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

public ConstraintSet getConstraintSet(Context context, java.lang.String id)

Get the constraintSet given the id

Parameters:

context:
id:

Returns:

public void setConstraintSet(int id, ConstraintSet set)

Maps the Constraint set to the id.

Parameters:

id: - unique id to represent the ConstraintSet
set: - ConstraintSet to be represented with the id.

public void getKeyFrames(MotionController motionController)

provides the key frames & CycleFrames to the motion view to

Parameters:

motionController:

public void setKeyframe(View view, int position, java.lang.String name, java.lang.Object value)

Set a keyFrame on the current Transition

Parameters:

view:
position:
name:
value:

public float getPathPercent(View view, int position)

Get the path percent (Non functional currently)

Parameters:

view:
position:

Returns:

public Interpolator getInterpolator()

Get the interpolator define for the current Transition

Returns:

public int getDuration()

Get Duration of the current transition.

Returns:

duration in milliseconds

public void setDuration(int duration)

Sets the duration of the current transition or the default if there is no current transition

Parameters:

duration: in milliseconds

public int gatPathMotionArc()

The transition arc path mode

Returns:

public float getStaggered()

Get the staggered value of the current transition. Will default to 0 staggered if there is no current transition.

Returns:

public static java.lang.String stripID(java.lang.String id)

Utility to strip the @id/ from an id

Parameters:

id:

Returns:

public int lookUpConstraintId(java.lang.String id)

Used at design time

Parameters:

id:

Returns:

public java.lang.String lookUpConstraintName(int id)

used at design time

Returns:

public void disableAutoTransition(boolean disable)

this allow disabling autoTransitions to prevent design surface from being in undefined states

Parameters:

disable:

Source

/*
 * 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)) {
                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
     * @DoNotShow
     */
    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
         * deprecated This method should be called {@code setEnabled}, so that {@code isEnabled}
         * can be accessed as a <a href="https://developer.android.com/kotlin/interop#property_prefixes">property from Kotlin</a>.
         * Use {@link #setEnabled(boolean)} instead.
         */
        private void setEnable(boolean enable) {
            setEnabled(enable);
        }

        /**
         * 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);
        String document = null;
        String tagName = null;
        try {
            Transition transition = null;
            for (int eventType = parser.getEventType();
                    eventType != XmlResourceParser.END_DOCUMENT;
                    eventType = parser.next()) {
                switch (eventType) {
                    case XmlResourceParser.START_DOCUMENT:
                        document = parser.getName();
                        break;
                    case XmlResourceParser.START_TAG:
                        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;
                    case XmlResourceParser.END_TAG:
                        tagName = null;
                        break;
                    case XmlResourceParser.TEXT:
                        break;
                }
            }
        } catch (XmlPullParserException e) {
            if (DEBUG) {
                Log.v(TAG, getLine(context, resourceId, parser) + " " + e.getMessage());
            }
            e.printStackTrace();
        } catch (IOException e) {
            if (DEBUG) {
                Log.v(TAG, getLine(context, resourceId, parser) + " " + e.getMessage());
            }
            e.printStackTrace();
        }
    }

    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());
            }
            e.printStackTrace();
        } catch (IOException e) {
            if (DEBUG) {
                Log.v(TAG, getLine(context, resourceId, includeParser)
                        + " " + e.getMessage());
            }
            e.printStackTrace();
        }
        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() + "\"";
    }
}