public class

MotionLayout

extends ConstraintLayout

implements NestedScrollingParent3

 java.lang.Object

↳ViewGroup

androidx.constraintlayout.widget.ConstraintLayout

↳androidx.constraintlayout.motion.widget.MotionLayout

Gradle dependencies

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

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

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

Androidx artifact mapping:

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

Overview

A subclass of ConstraintLayout that supports animating between various states Added in 2.0

A MotionLayout is a subclass of ConstraintLayout which supports transitions between between various states (ConstraintSet) defined in MotionScenes.

Note: MotionLayout is available as a support library that you can use on Android systems starting with API level 14 (ICS).

MotionLayout links to and requires a MotionScene file. The file contains one top level tag "MotionScene"

LayoutDescription

TagsDescription
Describes states supported by the system (optional)
Describes a constraint set
Describes a transition between two states or ConstraintSets
Describes a transition of a View within a states or ConstraintSets

Transition

AttributesDescription
android:id The id of the Transition
constraintSetStart ConstraintSet to be used as the start constraints or a layout file to get the constraint from
constraintSetEnd ConstraintSet to be used as the end constraints or a layout file to get the constraint from
motionInterpolator The ability to set an overall interpolation (easeInOut, linear, etc.)
duration Length of time to take to perform the transition
staggered Overrides the Manhattan distance from the top most view in the list of views.
  • For any view of stagger value S(Vi)
  • With the transition stagger value of TS (from 0.0 - 1.0)
  • The duration of the animation is duration
  • The views animation duration DS = duration * (1 -TS)
  • Call the stagger fraction SFi = (S(Vi) - S(V0)) / (S(Vn) - S(V0))
  • The view starts animating at: (duration-DS) * SFi
pathMotionArc The path will move in arc (quarter ellipses) key words {startVertical | startHorizontal | flip | none }
autoTransition automatically transition from one state to another. key words {none, jumpToStart, jumpToEnd, animateToStart, animateToEnd}
transitionFlags flags that adjust the behaviour of Transitions. supports {none, beginOnFirstDraw} begin on first draw forces the transition's clock to start when it is first displayed not when the begin is called
layoutDuringTransition Configures MotionLayout on how to react to requestLayout calls during transitions. Allowed values are {ignoreRequest, honorRequest}
Adds support for touch handling (optional)
Adds support for triggering transition (optional)
Describes a set of Key object which modify the animation between constraint sets.
  • A transition is typically defined by specifying its start and end ConstraintSets. You also have the possibility to not specify them, in which case such transition will become a Default transition. That Default transition will be applied between any state change that isn't explicitly covered by a transition.
  • The starting state of the MotionLayout is defined to be the constraintSetStart of the first transition.
  • If no transition is specified (or only a default Transition) the MotionLayout tag must contain a app:currentState to define the starting state of the MotionLayout

ViewTransition

AttributesDescription
android:id The id of the ViewTransition
viewTransitionMode currentState, allStates, noState transition affect the state of the view in the current constraintSet or all ConstraintSets or non if noState the ViewTransitions are run asynchronous
onStateTransition actionDown or actionUp run transition if on touch down or up if view matches motionTarget
motionInterpolator The ability to set an overall interpolation key words {easeInOut, linear, etc.}
duration Length of time to take to perform the ViewTransition
pathMotionArc The path will move in arc (quarter ellipses) key words {startVertical | startHorizontal | flip | none }
motionTarget Apply ViewTransition matching this string or id.
setsTag set this tag at end of transition
clearsTag clears this tag at end of transition
ifTagSet run transition if this tag is set on view
ifTagNotSet run transition if this tag is not set on view/td>
Adds support for touch handling (optional)
Adds support for triggering transition (optional)
Describes a set of Key object which modify the animation between constraint sets.
  • A Transition is typically defined by specifying its start and end ConstraintSets. You also have the possibility to not specify them, in which case such transition will become a Default transition. That Default transition will be applied between any state change that isn't explicitly covered by a transition.
  • The starting state of the MotionLayout is defined to be the constraintSetStart of the first transition.
  • If no transition is specified (or only a default Transition) the MotionLayout tag must contain a app:currentState to define the starting state of the MotionLayout

OnSwipe (optional)

AttributesDescription
touchAnchorId Have the drag act as if it is moving the "touchAnchorSide" of this object
touchRegionId Limits the region that the touch can be start in to the bounds of this view (even if the view is invisible)
touchAnchorSide The side of the object to move with {top|left|right|bottom}
maxVelocity limit the maximum velocity (in progress/sec) of the animation will on touch up. Default 4
dragDirection which side to swipe from {dragUp|dragDown|dragLeft|dragRight}
maxAcceleration how quickly the animation will accelerate (progress/sec/sec) and decelerate on touch up. Default 1.2
dragScale scale factor to adjust the swipe by. (e.g. 0.5 would require you to move 2x as much)
dragThreshold How much to drag before swipe gesture runs. Important for mult-direction swipe. Default is 10. 1 is very sensitive.
moveWhenScrollAtTop If the swipe is scrolling and View (such as RecyclerView or NestedScrollView) do scroll and transition happen at the same time
onTouchUp Support for various swipe modes autoComplete,autoCompleteToStart,autoCompleteToEnd,stop,decelerate,decelerateAndComplete

OnClick (optional)

AttributesDescription
motionTarget What view triggers Transition.
clickAction Direction for buttons to move the animation. Or (|) combination of: toggle, transitionToEnd, transitionToStart, jumpToEnd, jumpToStart

StateSet

defaultState The constraint set or layout to use
The side of the object to move

State

android:id Id of the State
constraints Id of the ConstraintSet or the Layout file
a different constraintSet/layout to choose if the with or height matches

Variant

region_widthLessThan Match if width less than
region_widthMoreThan Match if width more than
region_heightLessThan Match if height less than
region_heightMoreThan Match if height more than
constraints Id of the ConstraintSet or layout

ConstraintSet

android:id The id of the ConstraintSet
deriveConstraintsFrom The id of another constraintSet which defines the constraints not define in this set. If not specified the layout defines the undefined constraints.
A ConstraintLayout Constraints + other attributes associated with a view

Constraint

Constraint supports two forms:

1: All of ConstraintLayout + the ones listed below + .

Or

2: Combination of tags: . The advantage of using these is that if not present the attributes are taken from the base layout file. This saves from replicating all the layout tags if only a Motion tag is needed. If is used then all layout attributes in the base are ignored.

android:id Id of the View
[ConstraintLayout attributes] Any attribute that is part of ConstraintLayout layout is allowed
[Standard View attributes] A collection of view attributes supported by the system (see below)
transitionEasing define an easing curve to be used when animating from this point (e.g. curve(1.0,0,0,1.0)) or key words {standard | accelerate | decelerate | linear}
pathMotionArc the path will move in arc (quarter ellipses) or key words {startVertical | startHorizontal | none }
transitionPathRotate (float) rotate object relative to path taken
drawPath draw the path the layout will animate animate
progress call method setProgress(float) on this view (used to talk to nested ConstraintLayouts etc.)
call a set"name" method via reflection
Attributes for the ConstraintLayout e.g. layout_constraintTop_toTopOf
currently only visibility, alpha, motionProgress,layout_constraintTag.
All the view transform API such as android:rotation.
Motion Layout control commands such as transitionEasing and pathMotionArc

Layout

[ConstraintLayout attributes] Also see for attributes

PropertySet

visibility set the Visibility of the view. One of Visible, invisible or gone
alpha setAlpha value
motionProgress using reflection call setProgress
layout_constraintTag a tagging string to identify the type of object

Transform

android:elevation base z depth of the view.
android:rotation rotation of the view, in degrees.
android:rotationX rotation of the view around the x axis, in degrees.
android:rotationY rotation of the view around the y axis, in degrees.
android:scaleX scale of the view in the x direction
android:scaleY scale of the view in the y direction.
android:translationX translation in x of the view. This value is added post-layout to the left property of the view, which is set by its layout.
android:translationY translation in y of the view. This value is added post-layout to th e top property of the view, which is set by its layout
android:translationZ translation in z of the view. This value is added to its elevation.

Motion

transitionEasing Defines an acceleration curve.
pathMotionArc Says the object should move in a quarter ellipse unless the motion is vertical or horizontal
motionPathRotate set the rotation to the path of the object + this angle.
drawPath Debugging utility to draw the motion of the path

CustomAttribute

attributeName The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)
customColorValue The value is a color looking setMyAttr(int )
customIntegerValue The value is an integer looking setMyAttr(int )
customFloatValue The value is a float looking setMyAttr(float )
customStringValue The value is a String looking setMyAttr(String )
customDimension The value is a dimension looking setMyAttr(float )
customBoolean The value is true or false looking setMyAttr(boolean )

KeyFrameSet

This is the container for a collection of Key objects (such as KeyPosition) which provide information about how the views should move

Controls the layout position during animation
Controls the post layout properties during animation
Controls oscillations with respect to position of post layout properties during animation
Controls oscillations with respect to time of post layout properties during animation
trigger callbacks into code at fixed point during the animation

KeyPosition

motionTarget Id of the View or a regular expression to match layout_ConstraintTag
framePosition The point along the interpolation 0 = start 100 = end
transitionEasing define an easing curve to be used when animating from this point (e.g. curve(1.0,0,0, 1.0)) or key words {standard | accelerate | decelerate | linear }
pathMotionArc The path will move in arc (quarter ellipses) key words {startVertical | startHorizontal | flip | none }
keyPositionType how this keyframe's deviation for linear path is calculated {deltaRelative | pathRelative|parentRelative}
percentX (float) percent distance from start to end along X axis (deltaRelative) or along the path in pathRelative
percentY (float) Percent distance from start to end along Y axis (deltaRelative) or perpendicular to path in pathRelative
percentWidth (float) Percent of change in the width. Note if the width does not change this has no effect.This overrides sizePercent.
percentHeight (float) Percent of change in the width. Note if the width does not change this has no effect.This overrides sizePercent.
curveFit path is traced
drawPath Draw the path of the objects layout takes useful for debugging
sizePercent If the view changes size this controls how growth of the size. (for fixed size objects use KeyAttributes scaleX/X)
curveFit selects a path based on straight lines or a path based on a monotonic spline {linear|spline}

KeyAttribute

motionTarget Id of the View or a regular expression to match layout_ConstraintTag
framePosition The point along the interpolation 0 = start 100 = end
curveFit selects a path based on straight lines or a path based on a monotonic spline {linear|spline}
transitionEasing Define an easing curve to be used when animating from this point (e.g. curve(1.0,0,0, 1.0)) or key words {standard | accelerate | decelerate | linear }
transitionPathRotate (float) rotate object relative to path taken
drawPath draw the path the layout will animate animate
motionProgress call method setProgress(float) on this view (used to talk to nested ConstraintLayouts etc.)
[standard view attributes](except visibility) A collection of post layout view attributes see below
call a set"name" method via reflection

CustomAttribute

attributeName The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)
customColorValue The value is a color looking setMyAttr(int )
customIntegerValue The value is an integer looking setMyAttr(int )
customFloatValue The value is a float looking setMyAttr(float )
customStringValue The value is a String looking setMyAttr(String )
customDimension The value is a dimension looking setMyAttr(float )
customBoolean The value is true or false looking setMyAttr(boolean )

KeyCycle

motionTarget Id of the View or a regular expression to match layout_ConstraintTag
framePosition The point along the interpolation 0 = start 100 = end
[Standard View attributes] A collection of view attributes supported by the system (see below)
waveShape The shape of the wave to generate {sin|square|triangle|sawtooth|reverseSawtooth|cos|bounce}
wavePeriod The number of cycles to loop near this region
waveOffset offset value added to the attribute
transitionPathRotate Cycles applied to rotation relative to the path the view is travelling
progress call method setProgress(float) on this view (used to talk to nested ConstraintLayouts etc.)
call a set"name" method via reflection (limited to floats)

CustomAttribute

attributeName The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)
customFloatValue The value is a float looking setMyAttr(float )

KeyTimeCycle

motionTarget Id of the View or a regular expression to match layout_ConstraintTag
framePosition The point along the interpolation 0 = start 100 = end
[Standard View attributes] A collection of view attributes supported by the system (see below)
waveShape The shape of the wave to generate {sin|square|triangle|sawtooth|reverseSawtooth|cos|bounce}
wavePeriod The number of cycles per second
waveOffset offset value added to the attribute
transitionPathRotate Cycles applied to rotation relative to the path the view is travelling
progress call method setProgress(float) on this view (used to talk to nested ConstraintLayouts etc.)
call a set"name" method via reflection (limited to floats)

CustomAttribute

attributeName The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)
customFloatValue The value is a float looking setMyAttr(float )

KeyTrigger

motionTarget Id of the View or a regular expression to match layout_ConstraintTag
framePosition The point along the interpolation 0 = start 100 = end
onCross (method name) on crossing this position call this methods on the t arget
onPositiveCross (method name) on forward crossing of the framePosition call this methods on the target
onNegativeCross/td> (method name) backward crossing of the framePosition call this methods on the target
viewTransitionOnCross (ViewTransition Id) start a NoState view transition on crossing or hitting target
viewTransitionOnPositiveCross (ViewTransition Id) start a NoState view transition forward crossing of the framePosition or entering target
viewTransitionOnNegativeCross/td> (ViewTransition Id) start a NoState view transition backward crossing of the framePosition or leaving target
triggerSlack (float) do not call trigger again if the framePosition has not moved this fraction away from the trigger point
triggerId (id) call the TransitionListener with this trigger id
motion_postLayoutCollision Define motion pre or post layout. Post layout is more expensive but captures KeyAttributes or KeyCycle motions.
motion_triggerOnCollision (id) Trigger if the motionTarget collides with the other motionTarget

Standard attributes

android:visibility Android view attribute that
android:alpha Android view attribute that
android:elevation base z depth of the view.
android:rotation rotation of the view, in degrees.
android:rotationX rotation of the view around the x axis, in degrees.
android:rotationY rotation of the view around the y axis, in degrees.
android:scaleX scale of the view in the x direction.
android:scaleY scale of the view in the y direction.
android:translationX translation in x of the view.
android:translationY translation in y of the view.
android:translationZ translation in z of the view.

Summary

Fields
public static final intDEBUG_SHOW_NONE

public static final intDEBUG_SHOW_PATH

public static final intDEBUG_SHOW_PROGRESS

public static booleanIS_IN_EDIT_MODE

protected booleanmMeasureDuringTransition

public static final intTOUCH_UP_COMPLETE

public static final intTOUCH_UP_COMPLETE_TO_END

public static final intTOUCH_UP_COMPLETE_TO_START

public static final intTOUCH_UP_DECELERATE

public static final intTOUCH_UP_DECELERATE_AND_COMPLETE

public static final intTOUCH_UP_NEVER_TO_END

public static final intTOUCH_UP_NEVER_TO_START

public static final intTOUCH_UP_STOP

public static final intVELOCITY_LAYOUT

public static final intVELOCITY_POST_LAYOUT

public static final intVELOCITY_STATIC_LAYOUT

public static final intVELOCITY_STATIC_POST_LAYOUT

from ConstraintLayoutDESIGN_INFO_ID, mConstraintLayoutSpec, mDirtyHierarchy, mLayoutWidget, VERSION
Constructors
publicMotionLayout(Context context)

publicMotionLayout(Context context, AttributeSet attrs)

publicMotionLayout(Context context, AttributeSet attrs, int defStyleAttr)

Methods
public voidaddTransitionListener(MotionLayout.TransitionListener listener)

adds a listener to be notified of drawer events.

public booleanapplyViewTransition(int viewTransitionId, MotionController motionController)

Apply the view transitions keyFrames to the MotionController.

public ConstraintSetcloneConstraintSet(int id)

Creates a ConstraintSet based on an existing constraintSet.

protected voiddispatchDraw(Canvas canvas)

Used to draw debugging graphics and to do post layout changes

public voidenableTransition(int transitionID, boolean enable)

Disable the transition based on transitionID

public voidenableViewTransition(int viewTransitionId, boolean enable)

Enable a ViewTransition ID.

protected voidfireTransitionCompleted()

This causes the callback TransitionCompleted to be called

public voidfireTrigger(int triggerId, boolean positive, float progress)

This causes the callback onTransitionTrigger to be called

public ConstraintSetgetConstraintSet(int id)

Get the ConstraintSet associated with an id This returns a link to the constraintSet But in most cases can be used.

public int[]getConstraintSetIds()

Get the id's of all constraintSets used by MotionLayout

public intgetCurrentState()

Return the current state id

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

Get all Transitions known to the system.

public DesignToolgetDesignTool()

public intgetEndState()

Gets the state you are currently transition to.

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

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

protected longgetNanoTime()

Subclasses can override to define testClasses

public floatgetProgress()

Get current position during an animation.

public MotionScenegetScene()

Get the motion scene of the layout.

public intgetStartState()

Gets the state you are currently transitioning from.

public floatgetTargetPosition()

Gets the position you are animating to typically 0 or 1.

public MotionScene.TransitiongetTransition(int id)

This returns the internal Transition Structure

public BundlegetTransitionState()

public longgetTransitionTimeMs()

Gets the time of the currently set animation.

public floatgetVelocity()

Returns the last velocity used in the transition

public voidgetViewVelocity(View view, float posOnViewX, float posOnViewY, float[] returnVelocity[], int type)

Returns the last layout velocity used in the transition

public booleanisDelayedApplicationOfInitialState()

Is initial state changes are applied during onAttachedToWindow or after.

public booleanisInRotation()

public booleanisInteractionEnabled()

Determines whether MotionLayout's touch & click handling are enabled.

public booleanisViewTransitionEnabled(int viewTransitionId)

Is transition id enabled or disabled

public voidjumpToState(int id)

This jumps to a state It will be at that state after one repaint cycle If the current transition contains that state.

public voidloadLayoutDescription(int motionScene)

This overrides ConstraintLayout and only accepts a MotionScene.

protected MotionLayout.MotionTrackerobtainVelocityTracker()

Subclasses can override to build test frameworks

protected voidonAttachedToWindow()

public booleanonInterceptTouchEvent(MotionEvent event)

Intercepts the touch event to correctly handle touch region id handover

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

protected voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)

public booleanonNestedFling(View target, float velocityX, float velocityY, boolean consumed)

public booleanonNestedPreFling(View target, float velocityX, float velocityY)

public voidonNestedPreScroll(View target, int dx, int dy, int[] consumed[], int type)

public voidonNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type)

public voidonNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, int[] consumed[])

public voidonNestedScrollAccepted(View child, View target, int axes, int type)

public voidonRtlPropertiesChanged(int layoutDirection)

public booleanonStartNestedScroll(View child, View target, int axes, int type)

public voidonStopNestedScroll(View target, int type)

public booleanonTouchEvent(MotionEvent event)

public voidonViewAdded(View view)

public voidonViewRemoved(View view)

protected voidparseLayoutDescription(int id)

block ConstraintLayout from handling layout description

public voidrebuildMotion()

rebuild the motion Layouts

public voidrebuildScene()

rebuild the motion Layouts

public booleanremoveTransitionListener(MotionLayout.TransitionListener listener)

adds a listener to be notified of drawer events.

public voidrequestLayout()

public voidrotateTo(int id, int duration)

Rotate the layout based on the angle to a ConstraintSet

public voidscheduleTransitionTo(int id)

on completing the current transition, transition to this state.

public voidsetDebugMode(int debugMode)

Display the debugging information such as paths information

public voidsetDelayedApplicationOfInitialState(boolean delayedApply)

Initial state changes are applied during onAttachedToWindow unless this is set to true.

public voidsetInteractionEnabled(boolean enabled)

Enables (or disables) MotionLayout's onClick and onSwipe handling.

public voidsetInterpolatedProgress(float pos)

Set the transition position between 0 an 1

public voidsetOnHide(float progress)

Notify OnHide motion helpers

public voidsetOnShow(float progress)

Notify OnShow motion helpers

public voidsetProgress(float pos)

Set the transition position between 0 an 1

public voidsetProgress(float pos, float velocity)

Set the transition position between 0 an 1

public voidsetScene(MotionScene scene)

Sets a motion scene to the layout.

public voidsetState(int id, int screenWidth, int screenHeight)

Set the State of the Constraint layout.

public voidsetTransition(int transitionId)

Set a transition explicitly to a Transition that has an ID The transition must have been named with android:id=...

public voidsetTransition(int beginId, int endId)

Set a transition explicitly between two constraint sets

protected voidsetTransition(MotionScene.Transition transition)

public voidsetTransitionDuration(int milliseconds)

Change the current Transition duration.

public voidsetTransitionListener(MotionLayout.TransitionListener listener)

Set a listener to be notified of drawer events.

public voidsetTransitionState(Bundle bundle)

Set the transition state as a bundle

public java.lang.StringtoString()

public voidtouchAnimateTo(int touchUpMode, float position, float currentVelocity)

public voidtouchSpringTo(float position, float currentVelocity)

Allows you to use trigger spring motion touch behaviour.

public voidtransitionToEnd()

Animate to the ending position of the current transition.

public voidtransitionToEnd(java.lang.Runnable onComplete)

Animate to the ending position of the current transition.

public voidtransitionToStart()

Animate to the starting position of the current transition.

public voidtransitionToStart(java.lang.Runnable onComplete)

Animate to the starting position of the current transition.

public voidtransitionToState(int id)

Animate to the state defined by the id.

public voidtransitionToState(int id, int duration)

Animate to the state defined by the id.

public voidtransitionToState(int id, int screenWidth, int screenHeight)

Animate to the state defined by the id.

public voidtransitionToState(int id, int screenWidth, int screenHeight, int duration)

Animate to the state defined by the id.

public voidupdateState()

Not sure we want this

public voidupdateState(int stateId, ConstraintSet set)

update a ConstraintSet under the id.

public voidupdateStateAnimate(int stateId, ConstraintSet set, int duration)

Update a ConstraintSet but animate the change.

public voidviewTransition(int viewTransitionId, View view[])

Execute a ViewTransition.

from ConstraintLayoutaddValueModifier, applyConstraintsFromLayoutParams, checkLayoutParams, dynamicUpdateConstraints, fillMetrics, forceLayout, generateDefaultLayoutParams, generateLayoutParams, generateLayoutParams, getDesignInformation, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOptimizationLevel, getSceneString, getSharedValues, getViewById, getViewWidget, isRtl, resolveMeasuredDimension, resolveSystem, setConstraintSet, setDesignInformation, setId, setMaxHeight, setMaxWidth, setMinHeight, setMinWidth, setOnConstraintsChanged, setOptimizationLevel, setSelfDimensionBehaviour, shouldDelayChildPressedState
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, wait, wait, wait

Fields

public static final int TOUCH_UP_COMPLETE

public static final int TOUCH_UP_COMPLETE_TO_START

public static final int TOUCH_UP_COMPLETE_TO_END

public static final int TOUCH_UP_STOP

public static final int TOUCH_UP_DECELERATE

public static final int TOUCH_UP_DECELERATE_AND_COMPLETE

public static final int TOUCH_UP_NEVER_TO_START

public static final int TOUCH_UP_NEVER_TO_END

public static boolean IS_IN_EDIT_MODE

public static final int DEBUG_SHOW_NONE

public static final int DEBUG_SHOW_PROGRESS

public static final int DEBUG_SHOW_PATH

public static final int VELOCITY_POST_LAYOUT

public static final int VELOCITY_LAYOUT

public static final int VELOCITY_STATIC_POST_LAYOUT

public static final int VELOCITY_STATIC_LAYOUT

protected boolean mMeasureDuringTransition

Constructors

public MotionLayout(Context context)

public MotionLayout(Context context, AttributeSet attrs)

public MotionLayout(Context context, AttributeSet attrs, int defStyleAttr)

Methods

protected long getNanoTime()

Subclasses can override to define testClasses

Returns:

protected MotionLayout.MotionTracker obtainVelocityTracker()

Subclasses can override to build test frameworks

Returns:

public void enableTransition(int transitionID, boolean enable)

Disable the transition based on transitionID

Parameters:

transitionID:
enable:

public void setTransition(int beginId, int endId)

Set a transition explicitly between two constraint sets

Parameters:

beginId: the id of the start constraint set
endId: the id of the end constraint set

public void setTransition(int transitionId)

Set a transition explicitly to a Transition that has an ID The transition must have been named with android:id=...

Parameters:

transitionId: the id to set

protected void setTransition(MotionScene.Transition transition)

public void loadLayoutDescription(int motionScene)

This overrides ConstraintLayout and only accepts a MotionScene.

Parameters:

motionScene: The resource id, or 0 to reset the MotionScene.

public void setState(int id, int screenWidth, int screenHeight)

Set the State of the Constraint layout. Causing it to load a particular ConstraintSet. for states with variants the variant with matching width and height constraintSet will be chosen

Parameters:

id: set the state width and height
screenWidth:
screenHeight:

public void setInterpolatedProgress(float pos)

Set the transition position between 0 an 1

Parameters:

pos:

public void setProgress(float pos, float velocity)

Set the transition position between 0 an 1

Parameters:

pos:
velocity:

public void setTransitionState(Bundle bundle)

Set the transition state as a bundle

public Bundle getTransitionState()

Returns:

bundle containing start and end state

public void setProgress(float pos)

Set the transition position between 0 an 1

Parameters:

pos: the position in the transition from 0...1

public void touchAnimateTo(int touchUpMode, float position, float currentVelocity)

Parameters:

touchUpMode: behavior on touch up, can be either:

  • TOUCH_UP_COMPLETE (default) : will complete the transition, picking up automatically a correct velocity to do so
  • TOUCH_UP_STOP : will allow stopping mid-transition
  • TOUCH_UP_DECELERATE : will slowly decay, possibly past the transition (i.e. it will do a hard stop if unmanaged)
  • TOUCH_UP_DECELERATE_AND_COMPLETE : will automatically pick between TOUCH_UP_COMPLETE and TOUCH_UP_DECELERATE
, TOUCH_UP_STOP (will allow stopping
position: animate to given position
currentVelocity:

public void touchSpringTo(float position, float currentVelocity)

Allows you to use trigger spring motion touch behaviour. You must have configured all the spring parameters in the Transition's OnSwipe

Parameters:

position: the position 0 - 1
currentVelocity: the current velocity rate of change in position per second

public void transitionToStart()

Animate to the starting position of the current transition. This will not work during on create as there is no transition Transitions are only set up during onAttach

public void transitionToStart(java.lang.Runnable onComplete)

Animate to the starting position of the current transition. This will not work during on create as there is no transition Transitions are only set up during onAttach

Parameters:

onComplete: callback when task is done

public void transitionToEnd()

Animate to the ending position of the current transition. This will not work during on create as there is no transition Transitions are only set up during onAttach

public void transitionToEnd(java.lang.Runnable onComplete)

Animate to the ending position of the current transition. This will not work during on create as there is no transition Transitions are only set up during onAttach

Parameters:

onComplete: callback when task is done

public void transitionToState(int id)

Animate to the state defined by the id. The id is the id of the ConstraintSet or the id of the State.

Parameters:

id: the state to transition to

public void transitionToState(int id, int duration)

Animate to the state defined by the id. The id is the id of the ConstraintSet or the id of the State.

Parameters:

id: the state to transition to
duration: time in ms. if 0 set by default or transition -1 by current

public void transitionToState(int id, int screenWidth, int screenHeight)

Animate to the state defined by the id. Width and height may be used in the picking of the id using this StateSet.

Parameters:

id: the state to transition
screenWidth: the with of the motionLayout used to select the variant
screenHeight: the height of the motionLayout used to select the variant

public void rotateTo(int id, int duration)

Rotate the layout based on the angle to a ConstraintSet

Parameters:

id: constraintSet
duration: time to take to rotate

public boolean isInRotation()

public void jumpToState(int id)

This jumps to a state It will be at that state after one repaint cycle If the current transition contains that state. It setsProgress 0 or 1 to that state. If not in the current transition itsl

Parameters:

id: state to set

public void transitionToState(int id, int screenWidth, int screenHeight, int duration)

Animate to the state defined by the id. Width and height may be used in the picking of the id using this StateSet.

Parameters:

id: the state to transition
screenWidth: the with of the motionLayout used to select the variant
screenHeight: the height of the motionLayout used to select the variant
duration: time in ms. if 0 set by default or transition -1 by current

public float getVelocity()

Returns the last velocity used in the transition

Returns:

public void getViewVelocity(View view, float posOnViewX, float posOnViewY, float[] returnVelocity[], int type)

Returns the last layout velocity used in the transition

Parameters:

view: The view
posOnViewX: The x position on the view
posOnViewY: The y position on the view
returnVelocity: The velocity
type: Velocity returned 0 = post layout, 1 = layout, 2 = static postlayout

public void requestLayout()

public java.lang.String toString()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

public boolean onStartNestedScroll(View child, View target, int axes, int type)

public void onNestedScrollAccepted(View child, View target, int axes, int type)

public void onStopNestedScroll(View target, int type)

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, int[] consumed[])

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type)

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed[], int type)

public boolean onNestedPreFling(View target, float velocityX, float velocityY)

public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)

protected void dispatchDraw(Canvas canvas)

Used to draw debugging graphics and to do post layout changes

Parameters:

canvas:

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

protected void parseLayoutDescription(int id)

block ConstraintLayout from handling layout description

Parameters:

id:

public void setScene(MotionScene scene)

Sets a motion scene to the layout. Subsequent calls to it will override the previous scene.

public MotionScene getScene()

Get the motion scene of the layout. Warning! This gives you direct access to the internal state of the MotionLayout making it easy corrupt the state.

Returns:

the motion scene

public void setDebugMode(int debugMode)

Display the debugging information such as paths information

Parameters:

debugMode: integer representing various debug modes

public boolean onInterceptTouchEvent(MotionEvent event)

Intercepts the touch event to correctly handle touch region id handover

Parameters:

event:

Returns:

public boolean onTouchEvent(MotionEvent event)

protected void onAttachedToWindow()

public void onRtlPropertiesChanged(int layoutDirection)

public int getCurrentState()

Return the current state id

Returns:

current state id

public float getProgress()

Get current position during an animation.

Returns:

current position from 0.0 to 1.0 inclusive

public long getTransitionTimeMs()

Gets the time of the currently set animation.

Returns:

time in Milliseconds

public void setTransitionListener(MotionLayout.TransitionListener listener)

Set a listener to be notified of drawer events.

Parameters:

listener: Listener to notify when drawer events occur

See also: MotionLayout.TransitionListener

public void addTransitionListener(MotionLayout.TransitionListener listener)

adds a listener to be notified of drawer events.

Parameters:

listener: Listener to notify when drawer events occur

See also: MotionLayout.TransitionListener

public boolean removeTransitionListener(MotionLayout.TransitionListener listener)

adds a listener to be notified of drawer events.

Parameters:

listener: Listener to notify when drawer events occur

Returns:

true if it contained the specified listener

See also: MotionLayout.TransitionListener

public void fireTrigger(int triggerId, boolean positive, float progress)

This causes the callback onTransitionTrigger to be called

Parameters:

triggerId: The id set set with triggerID
positive: for positive transition edge
progress: the current progress

protected void fireTransitionCompleted()

This causes the callback TransitionCompleted to be called

public DesignTool getDesignTool()

public void onViewAdded(View view)

public void onViewRemoved(View view)

public void setOnShow(float progress)

Notify OnShow motion helpers

Parameters:

progress:

public void setOnHide(float progress)

Notify OnHide motion helpers

Parameters:

progress:

public int[] getConstraintSetIds()

Get the id's of all constraintSets used by MotionLayout

Returns:

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

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

Returns:

public ConstraintSet getConstraintSet(int id)

Get the ConstraintSet associated with an id This returns a link to the constraintSet But in most cases can be used. createConstraintSet makes a copy which is more expensive.

Parameters:

id: of the constraintSet

Returns:

ConstraintSet of MotionLayout

See also: MotionLayout.cloneConstraintSet(int)

public ConstraintSet cloneConstraintSet(int id)

Creates a ConstraintSet based on an existing constraintSet. This makes a copy of the ConstraintSet.

Parameters:

id: The ide of the ConstraintSet

Returns:

the ConstraintSet

public void rebuildMotion()

Deprecated: Please call rebuildScene() instead.

rebuild the motion Layouts

public void rebuildScene()

rebuild the motion Layouts

public void updateState(int stateId, ConstraintSet set)

update a ConstraintSet under the id.

Parameters:

stateId: id of the ConstraintSet
set: The constraintSet

public void updateStateAnimate(int stateId, ConstraintSet set, int duration)

Update a ConstraintSet but animate the change.

Parameters:

stateId: id of the ConstraintSet
set: The constraintSet
duration: The length of time to perform the animation

public void scheduleTransitionTo(int id)

on completing the current transition, transition to this state.

Parameters:

id:

public void updateState()

Not sure we want this

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

Get all Transitions known to the system.

Returns:

public int getStartState()

Gets the state you are currently transitioning from. If you are transitioning from an unknown state returns -1

Returns:

State you are transitioning from.

public int getEndState()

Gets the state you are currently transition to.

Returns:

The State you are transitioning to.

public float getTargetPosition()

Gets the position you are animating to typically 0 or 1. This is useful during animation after touch up

Returns:

The target position you are moving to

public void setTransitionDuration(int milliseconds)

Change the current Transition duration.

Parameters:

milliseconds: duration for transition to complete

public MotionScene.Transition getTransition(int id)

This returns the internal Transition Structure

Parameters:

id:

Returns:

public void setInteractionEnabled(boolean enabled)

Enables (or disables) MotionLayout's onClick and onSwipe handling.

Parameters:

enabled: If true, touch & click is enabled; otherwise it is disabled

public boolean isInteractionEnabled()

Determines whether MotionLayout's touch & click handling are enabled. An interaction enabled MotionLayout can respond to user input and initiate and control. MotionLayout interactions are enabled initially by default. MotionLayout touch & click handling may be enabled or disabled by calling its setInteractionEnabled method.

Returns:

true if MotionLayout's touch & click is enabled, false otherwise

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

Execute a ViewTransition. Transition will execute if its conditions are met and it is enabled

Parameters:

viewTransitionId:
view: The views to apply to

public void enableViewTransition(int viewTransitionId, boolean enable)

Enable a ViewTransition ID.

Parameters:

viewTransitionId: id of ViewTransition
enable: If false view transition cannot be executed.

public boolean isViewTransitionEnabled(int viewTransitionId)

Is transition id enabled or disabled

Parameters:

viewTransitionId: the ide of the transition

Returns:

true if enabled

public boolean applyViewTransition(int viewTransitionId, MotionController motionController)

Apply the view transitions keyFrames to the MotionController. Note ConstraintOverride is not used

Parameters:

viewTransitionId: the id of the view transition
motionController: the MotionController to apply the keyframes to

Returns:

true if it found and applied the viewTransition false otherwise

public boolean isDelayedApplicationOfInitialState()

Is initial state changes are applied during onAttachedToWindow or after.

Returns:

public void setDelayedApplicationOfInitialState(boolean delayedApply)

Initial state changes are applied during onAttachedToWindow unless this is set to true.

Parameters:

delayedApply:

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 static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

import static androidx.constraintlayout.motion.widget.MotionScene.Transition.TRANSITION_FLAG_FIRST_DRAW;
import static androidx.constraintlayout.motion.widget.MotionScene.Transition.TRANSITION_FLAG_INTERCEPT_TOUCH;
import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
import static androidx.constraintlayout.widget.ConstraintSet.UNSET;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.view.Display;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.TextView;

import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.core.motion.utils.KeyCache;
import androidx.constraintlayout.core.widgets.ConstraintAnchor;
import androidx.constraintlayout.core.widgets.ConstraintWidget;
import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer;
import androidx.constraintlayout.core.widgets.Flow;
import androidx.constraintlayout.core.widgets.Helper;
import androidx.constraintlayout.core.widgets.Placeholder;
import androidx.constraintlayout.motion.utils.StopLogic;
import androidx.constraintlayout.motion.utils.ViewState;
import androidx.constraintlayout.widget.Barrier;
import androidx.constraintlayout.widget.ConstraintHelper;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.Constraints;
import androidx.constraintlayout.widget.R;
import androidx.core.view.NestedScrollingParent3;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;


/**
 * A subclass of ConstraintLayout that supports animating between
 * various states <b>Added in 2.0</b>
 * <p>
 * A {@code MotionLayout} is a subclass of {@link ConstraintLayout}
 * which supports transitions between between various states ({@link ConstraintSet})
 * defined in {@link MotionScene}s.
 * <p>
 * <b>Note:</b> {@code MotionLayout} is available as a support library that you can use
 * on Android systems starting with API level 14 (ICS).
 * </p>
 * <p>
 * {@code MotionLayout} links to and requires a {@link MotionScene} file.
 * The file contains one top level tag "MotionScene"
 * <h2>LayoutDescription</h2>
 * <table summary="LayoutDescription">
 * <tr>
 * <th>Tags</th><th>Description</th>
 * </tr>
 * <tr>
 * <td>{@code <StateSet> }</td>
 * <td>Describes states supported by the system (optional)</td>
 * </tr>
 * <tr>
 * <td>{@code <ConstraintSet> }</td>
 * <td>Describes a constraint set</td>
 * </tr>
 * <tr>
 * <td>{@code <Transition> }</td>
 * <td>Describes a transition between two states or ConstraintSets</td>
 * </tr>
 * <tr>
 * <td>{@code <ViewTransition> }</td>
 * <td>Describes a transition of a View within a states or ConstraintSets</td>
 * </tr>
 * </table>
 *
 * <h2>Transition</h2>
 * <table summary="Transition attributes & tags">
 * <tr>
 * <th>Attributes</th><th>Description</th>
 * </tr>
 * <tr>
 * <td>android:id</td>
 * <td>The id of the Transition</td>
 * </tr>
 * <tr>
 * <td>constraintSetStart</td>
 * <td>ConstraintSet to be used as the start constraints or a
 * layout file to get the constraint from</td>
 * </tr>
 * <tr>
 * <td>constraintSetEnd</td>
 * <td>ConstraintSet to be used as the end constraints or a
 * layout file to get the constraint from</td>
 * </tr>
 * <tr>
 * <td>motionInterpolator</td>
 * <td>The ability to set an overall interpolation (easeInOut, linear, etc.)</td>
 * </tr>
 * <tr>
 * <td>duration</td>
 * <td>Length of time to take to perform the transition</td>
 * </tr>
 * <tr>
 * <td>staggered</td>
 * <td>Overrides the Manhattan distance from the top most view in the list of views.
 * <ul>
 *     <li>For any view of stagger value {@code S(Vi)}</li>
 *     <li>With the transition stagger value of {@code TS} (from 0.0 - 1.0)</li>
 *     <li>The duration of the animation is {@code duration}</li>
 *     <li>The views animation duration {@code DS = duration * (1 -TS)}</li>
 *     <li>Call the stagger fraction {@code SFi = (S(Vi) - S(V0)) / (S(Vn) - S(V0))}</li>
 *     <li>The view starts animating at: {@code (duration-DS) * SFi}</li>
 * </ul>
 * </td>
 * </tr>
 * <tr>
 * <td>pathMotionArc</td>
 * <td>The path will move in arc (quarter ellipses)
 * key words {startVertical | startHorizontal | flip | none }</td>
 * </tr>
 * <tr>
 * <td>autoTransition</td>
 * <td>automatically transition from one state to another.
 * key words {none, jumpToStart, jumpToEnd, animateToStart, animateToEnd}</td>
 * </tr>
 * </tr>
 * <tr>
 * <td>transitionFlags</td>
 * <td>flags that adjust the behaviour of Transitions. supports {none, beginOnFirstDraw}
 *      begin on first draw forces the transition's clock to start when it is first
 *      displayed not when the begin is called</td>
 * </tr>
 * </tr>
 * <tr>
 * <td>layoutDuringTransition</td>
 * <td>Configures MotionLayout on how to react to requestLayout calls during transitions.
 * Allowed values are {ignoreRequest, honorRequest}</td>
 * </tr>
 * <tr>
 * <td>{@code <OnSwipe> }</td>
 * <td>Adds support for touch handling (optional)</td>
 * </tr>
 * <tr>
 * <td>{@code <OnClick> }</td>
 * <td>Adds support for triggering transition (optional)</td>
 * </tr>
 * <tr>
 * <td>{@code <KeyFrameSet> }</td>
 * <td>Describes a set of Key object which modify the animation between constraint sets.</td>
 * </tr>
 * </table>
 *
 * <ul>
 * <li>A transition is typically defined by specifying its start and end ConstraintSets.
 * You also have the possibility to not specify them, in which case such transition
 * will become a Default transition.
 * That Default transition will be applied between any state change that isn't
 * explicitly covered by a transition.</li>
 * <li>The starting state of the MotionLayout is defined  to be the constraintSetStart of the first
 * transition.</li>
 * <li>If no transition is specified (or only a default Transition)
 * the MotionLayout tag must contain
 * a app:currentState to define the starting state of the MotionLayout</li>
 * </ul>
 *
 * <h2>ViewTransition</h2>
 * <table summary="Transition attributes & tags">
 * <tr>
 * <th>Attributes</th><th>Description</th>
 * </tr>
 * <tr>
 * <td>android:id</td>
 * <td>The id of the ViewTransition</td>
 * </tr>
 * <tr>
 * <td>viewTransitionMode</td>
 * <td>currentState, allStates, noState transition affect the state of the view
 * in the current constraintSet or all ConstraintSets or non
 *      if noState the ViewTransitions are run asynchronous</td>
 * </tr>
 * <tr>
 * <td>onStateTransition</td>
 * <td>actionDown or actionUp run transition if on touch down or
 * up if view matches motionTarget</td>
 * </tr>
 * <tr>
 * <td>motionInterpolator</td>
 * <td>The ability to set an overall interpolation
 * key words {easeInOut, linear, etc.}</td>
 * </tr>
 * <tr>
 * <td>duration</td>
 * <td>Length of time to take to perform the {@code ViewTransition}</td>
 * </tr>
 * <tr>
 * <td>pathMotionArc</td>
 * <td>The path will move in arc (quarter ellipses)
 * key words {startVertical | startHorizontal | flip | none }</td>
 * </tr>
 * <tr>
 * <td>motionTarget</td>
 * <td>Apply ViewTransition matching this string or id.</td>
 * </tr>
 * </tr>
 * <tr>
 * <td>setsTag</td>
 * <td>set this tag at end of transition</td>
 * </tr>
 * </tr>
 * <tr>
 * <td>clearsTag</td>
 * <td>clears this tag at end of transition</td>
 * </tr>
 * <tr>
 * <td>ifTagSet</td>
 * <td>run transition if this tag is set on view</td>
 * </tr>
 * <tr>
 * <td>ifTagNotSet</td>
 * <td>run transition if this tag is not set on view/td>
 * </tr>
 * <tr>
 * <td>{@code <OnSwipe> }</td>
 * <td>Adds support for touch handling (optional)</td>
 * </tr>
 * <tr>
 * <td>{@code <OnClick> }</td>
 * <td>Adds support for triggering transition (optional)</td>
 * </tr>
 * <tr>
 * <td>{@code <KeyFrameSet> }</td>
 * <td>Describes a set of Key object which modify the animation between constraint sets.</td>
 * </tr>
 * </table>
 *
 * <ul>
 * <li>A Transition is typically defined by specifying its start and end ConstraintSets.
 * You also have the possibility to not specify them, in which case such transition
 * will become a Default transition.
 * That Default transition will be applied between any state change that isn't
 * explicitly covered by a transition.</li>
 * <li>The starting state of the MotionLayout is defined to be the constraintSetStart of the first
 * transition.</li>
 * <li>If no transition is specified (or only a default Transition) the
 * MotionLayout tag must contain
 * a app:currentState to define the starting state of the MotionLayout</li>
 * </ul>
 *
 *
 * <p>
 * <h2>OnSwipe (optional)</h2>
 * <table summary="OnSwipe attributes">
 * <tr>
 * <th>Attributes</th><th>Description</th>
 * </tr>
 * <tr>
 * <td>touchAnchorId</td>
 * <td>Have the drag act as if it is moving the "touchAnchorSide" of this object</td>
 * </tr>
 * <tr>
 * <td>touchRegionId</td>
 * <td>Limits the region that the touch can be start in to the bounds of this view
 * (even if the view is invisible)</td>
 * </tr>
 * <tr>
 * <td>touchAnchorSide</td>
 * <td>The side of the object to move with {top|left|right|bottom}</td>
 * </tr>
 * <tr>
 * <td>maxVelocity</td>
 * <td>limit the maximum velocity (in progress/sec) of the animation will on touch up.
 * Default 4</td>
 * </tr>
 * <tr>
 * <td>dragDirection</td>
 * <td>which side to swipe from {dragUp|dragDown|dragLeft|dragRight}</td>
 * </tr>
 * <tr>
 * <td>maxAcceleration</td>
 * <td>how quickly the animation will accelerate
 * (progress/sec/sec) and decelerate on touch up. Default 1.2</td>
 * </tr>
 * <tr>
 * <td>dragScale</td>
 * <td>scale factor to adjust the swipe by. (e.g. 0.5 would require you to move 2x as much)</td>
 * </tr>
 * <td>dragThreshold</td>
 * <td>How much to drag before swipe gesture runs.
 * Important for mult-direction swipe. Default is 10. 1 is very sensitive.</td>
 * </tr>
 * <tr>
 * <td>moveWhenScrollAtTop</td>
 * <td>If the swipe is scrolling and View (such as RecyclerView or NestedScrollView)
 * do scroll and transition happen at the same time</td>
 * </tr>
 * <tr>
 * <td>onTouchUp</td>
 * <td>Support for various swipe modes
 * autoComplete,autoCompleteToStart,autoCompleteToEnd,stop,decelerate,decelerateAndComplete</td>
 * </tr>
 * </table>
 *
 * <p>
 * <h2>OnClick (optional)</h2>
 * <table summary="OnClick attributes">
 * <tr>
 * <th>Attributes</th><th>Description</th>
 * </tr>
 * <tr>
 * <td>motionTarget</td>
 * <td>What view triggers Transition.</td>
 * </tr>
 * <tr>
 * <td>clickAction</td>
 * <td>Direction for buttons to move the animation.
 * Or (|) combination of:  toggle, transitionToEnd, transitionToStart, jumpToEnd, jumpToStart</td>
 * </tr>
 * </table>
 *
 * <p>
 * <h2>StateSet</h2>
 * <table summary="StateSet tags & attributes">
 * <tr>
 * <td>defaultState</td>
 * <td>The constraint set or layout to use</td>
 * </tr>
 * <tr>
 * <td>{@code <State> }</td>
 * <td>The side of the object to move</td>
 * </tr>
 * </table>
 *
 * <p>
 * <h2>State</h2>
 * <table summary="State attributes">
 * <tr>
 * <td>android:id</td>
 * <td>Id of the State</td>
 * </tr>
 * <tr>
 * <td>constraints</td>
 * <td>Id of the ConstraintSet or the Layout file</td>
 * </tr>
 * <tr>
 * <td>{@code <Variant> }</td>
 * <td>a different constraintSet/layout to choose if the with or height matches</td>
 * </tr>
 * </table>
 *
 * <h2>Variant</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>region_widthLessThan</td>
 * <td>Match if width less than</td>
 * </tr>
 * <tr>
 * <td>region_widthMoreThan</td>
 * <td>Match if width more than</td>
 * </tr>
 * <tr>
 * <td>region_heightLessThan</td>
 * <td>Match if height less than</td>
 * </tr>
 * <tr>
 * <td>region_heightMoreThan</td>
 * <td>Match if height more than</td>
 * </tr>
 * <tr>
 * <td>constraints</td>
 * <td>Id of the ConstraintSet or layout</td>
 * </tr>
 * </table>
 *
 * <p>
 * <h2>ConstraintSet</h2>
 * <table summary="StateSet tags & attributes">
 * <tr>
 * <td>android:id</td>
 * <td>The id of the ConstraintSet</td>
 * </tr>
 * <tr>
 * <td>deriveConstraintsFrom</td>
 * <td>The id of another constraintSet which defines the constraints not define in this set.
 * If not specified the layout defines the undefined constraints.</td>
 * </tr>
 * <tr>
 * <td>{@code <Constraint> }</td>
 * <td>A ConstraintLayout Constraints + other attributes associated with a view</td>
 * </tr>
 * </table>
 *
 * <p>
 * <h2>Constraint</h2>
 * <p> Constraint supports two forms: <p>1: All of ConstraintLayout + the ones listed below +
 * {@code <CustomAttribute> }.</p>
 * <p>Or</p><p>
 * 2: Combination of tags: {@code <Layout> <PropertySet> <Transform> <Motion> <CustomAttribute> }.
 * The advantage of using these is that if not present the attributes are taken from the base
 * layout file. This saves from replicating all the layout tags if only a Motion tag is needed.
 * If <Layout> is used then all layout attributes in the base are ignored. </p>
 * </p>
 * <table summary="Constraint attributes">
 * <tr>
 * <td>android:id</td>
 * <td>Id of the View</td>
 * </tr>
 * <tr>
 * <td>[ConstraintLayout attributes]</td>
 * <td>Any attribute that is part of ConstraintLayout layout is allowed</td>
 * </tr>
 * <tr>
 * <td>[Standard View attributes]</td>
 * <td>A collection of view attributes supported by the system (see below)</td>
 * </tr>
 * <tr>
 * <td>transitionEasing</td>
 * <td>define an easing curve to be used when animating from this point
 * (e.g. {@code curve(1.0,0,0,1.0)})
 * or key words {standard | accelerate | decelerate | linear}</td>
 * </tr>
 * <tr>
 * <td>pathMotionArc</td>
 * <td>the path will move in arc (quarter ellipses)
 * or key words {startVertical | startHorizontal | none }</td>
 * </tr>
 * <tr>
 * <td>transitionPathRotate</td>
 * <td>(float) rotate object relative to path taken</td>
 * </tr>
 * <tr>
 * <td>drawPath</td>
 * <td>draw the path the layout will animate animate</td>
 * </tr>
 * <tr>
 * <td>progress</td>
 * <td>call method setProgress(float) on this  view
 * (used to talk to nested ConstraintLayouts etc.)</td>
 * </tr>
 * <tr>
 * <td>{@code <CustomAttribute> }</td>
 * <td>call a set"name" method via reflection</td>
 * </tr>
 * <tr>
 * <td>{@code <Layout> }</td>
 * <td>Attributes for the ConstraintLayout e.g. layout_constraintTop_toTopOf</td>
 * </tr>
 * <tr>
 * <td>{@code <PropertySet> }</td>
 * <td>currently only visibility, alpha, motionProgress,layout_constraintTag.</td>
 * </tr>
 * <tr>
 * <td>{@code <Transform> }</td>
 * <td>All the view transform API such as android:rotation.</td>
 * </tr>
 * <tr>
 * <td>{@code <Motion> }</td>
 * <td>Motion Layout control commands such as transitionEasing and pathMotionArc</td>
 * </tr>
 * </table>
 *
 * <p>
 * <p>
 * <h2>Layout</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>[ConstraintLayout attributes]</td>
 * <td>Also see {@link ConstraintLayout.LayoutParams
 * ConstraintLayout.LayoutParams} for attributes</td>
 * </tr>
 * </table>
 *
 * <h2>PropertySet</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>visibility</td>
 * <td>set the Visibility of the view. One of Visible, invisible or gone</td>
 * </tr>
 * <tr>
 * <td>alpha</td>
 * <td>setAlpha value</td>
 * </tr>
 * <tr>
 * <td>motionProgress</td>
 * <td>using reflection call setProgress</td>
 * </tr>
 * <tr>
 * <td>layout_constraintTag</td>
 * <td>a tagging string to identify the type of object</td>
 * </tr>
 * </table>
 *
 *
 * <h2>Transform</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>android:elevation</td>
 * <td>base z depth of the view.</td>
 * </tr>
 * <tr>
 * <td>android:rotation</td>
 * <td>rotation of the view, in degrees.</td>
 * </tr>
 * <tr>
 * <td>android:rotationX</td>
 * <td>rotation of the view around the x axis, in degrees.</td>
 * </tr>
 * <tr>
 * <td>android:rotationY</td>
 * <td>rotation of the view around the y axis, in degrees.</td>
 * </tr>
 * <tr>
 * <td>android:scaleX</td>
 * <td>scale of the view in the x direction</td>
 * </tr>
 * <tr>
 * <td>android:scaleY</td>
 * <td>scale of the view in the y direction.</td>
 * </tr>
 * <tr>
 * <td>android:translationX</td>
 * <td>translation in x of the view. This value is added post-layout to the  left
 * property of the view, which is set by its layout.</td>
 * </tr>
 * <tr>
 * <td>android:translationY</td>
 * <td>translation in y of the view. This value is added post-layout to th e top
 * property of the view, which is set by its layout</td>
 * </tr>
 * <tr>
 * <td>android:translationZ</td>
 * <td>translation in z of the view. This value is added to its elevation.</td>
 * </tr>
 * </table>
 *
 *
 * <h2>Motion</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>transitionEasing</td>
 * <td>Defines an acceleration curve.</td>
 * </tr>
 * <tr>
 * <td>pathMotionArc</td>
 * <td>Says the object should move in a quarter ellipse
 * unless the motion is vertical or horizontal</td>
 * </tr>
 * <tr>
 * <td>motionPathRotate</td>
 * <td>set the rotation to the path of the object + this angle.</td>
 * </tr>
 * <tr>
 * <td>drawPath</td>
 * <td>Debugging utility to draw the motion of the path</td>
 * </tr>
 * </table>
 *
 *
 * <h2>CustomAttribute</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>attributeName</td>
 * <td>The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)</td>
 * </tr>
 * <tr>
 * <td>customColorValue</td>
 * <td>The value is a color looking setMyAttr(int )</td>
 * </tr>
 * <tr>
 * <td>customIntegerValue</td>
 * <td>The value is an integer looking setMyAttr(int )</td>
 * </tr>
 * <tr>
 * <td>customFloatValue</td>
 * <td>The value is a float looking setMyAttr(float )</td>
 * </tr>
 * <tr>
 * <td>customStringValue</td>
 * <td>The value is a String looking setMyAttr(String )</td>
 * </tr>
 * <tr>
 * <td>customDimension</td>
 * <td>The value is a dimension looking setMyAttr(float )</td>
 * </tr>
 * <tr>
 * <td>customBoolean</td>
 * <td>The value is true or false looking setMyAttr(boolean )</td>
 * </tr>
 * </table>
 *
 * <p>
 * <p>
 * <h2>KeyFrameSet</h2>
 * <p> This is the container for a collection of Key objects (such as KeyPosition) which provide
 * information about how the views should move </p>
 * <table summary="StateSet tags & attributes">
 * <tr>
 * <td>{@code <KeyPosition>}</td>
 * <td>Controls the layout position during animation</td>
 * </tr>
 * <tr>
 * <td>{@code <KeyAttribute>}</td>
 * <td>Controls the post layout properties during animation</td>
 * </tr>
 * <tr>
 * <td>{@code <KeyCycle>}</td>
 * <td>Controls oscillations with respect to position
 * of post layout properties during animation</td>
 * </tr>
 * <tr>
 * <td>{@code <KeyTimeCycle>}</td>
 * <td>Controls oscillations with respect to time of post layout properties during animation</td>
 * </tr>
 * <tr>
 * <td>{@code <KeyTrigger>}</td>
 * <td>trigger callbacks into code at fixed point during the animation</td>
 * </tr>
 * </table>
 *
 * <p>
 * <h2>KeyPosition</h2>
 * <table summary="KeyPosition attributes">
 * <tr>
 * <td>motionTarget</td>
 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td>
 * </tr>
 * <tr>
 * <td>framePosition</td>
 * <td>The point along the interpolation 0 = start 100 = end</td>
 * </tr
 * <tr>
 * <td>transitionEasing</td>
 * <td>define an easing curve to be used when animating from this point (e.g. curve(1.0,0,0, 1.0))
 * or key words {standard | accelerate | decelerate | linear }
 * </td>
 * </tr>
 * <tr>
 * <td>pathMotionArc</td>
 * <td>The path will move in arc (quarter ellipses)
 * key words {startVertical | startHorizontal | flip | none }</td>
 * </tr>
 * <tr>
 * <td>keyPositionType</td>
 * <td>how this keyframe's deviation for linear path is calculated
 * {deltaRelative | pathRelative|parentRelative}</td>
 * </tr>
 * <tr>
 * <td>percentX</td>
 * <td>(float) percent distance from start to end along
 * X axis (deltaRelative) or along the path in pathRelative</td>
 * </tr>
 * <tr>
 * <td>percentY</td>
 * <td>(float) Percent distance from start to end along Y axis
 * (deltaRelative) or perpendicular to path in pathRelative</td>
 * </tr>
 * <tr>
 * <td>percentWidth</td>
 * <td>(float) Percent of change in the width.
 * Note if the width does not change this has no effect.This overrides sizePercent.</td>
 * </tr>
 * <tr>
 * <td>percentHeight</td>
 * <td>(float) Percent of change in the width.
 * Note if the width does not change this has no effect.This overrides sizePercent.</td>
 * </tr>
 * <tr>
 * <td>curveFit</td>
 * <td>path is traced</td>
 * </tr>
 * <tr>
 * <td>drawPath</td>
 * <td>Draw the path of the objects layout takes useful for debugging</td>
 * </tr>
 * <tr>
 * <td>sizePercent</td>
 * <td>If the view changes size this controls how growth of the  size.
 * (for fixed size objects use KeyAttributes scaleX/X)</td>
 * </tr>
 * <tr>
 * <td>curveFit</td>
 * <td>selects a path based on straight lines or a path based on a
 * monotonic spline {linear|spline}</td>
 * </tr>
 * </table>
 *
 * <p>
 * <p>
 * <h2>KeyAttribute</h2>
 * <table summary="KeyAttribute attributes">
 * <tr>
 * <td>motionTarget</td>
 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td>
 * </tr>
 * <tr>
 * <td>framePosition</td>
 * <td>The point along the interpolation 0 = start 100 = end</td>
 * </tr>
 * <tr>
 * <td>curveFit</td>
 * <td>selects a path based on straight lines or a path
 * based on a monotonic spline {linear|spline}</td>
 * </tr>
 * <tr>
 * <td>transitionEasing</td>
 * <td>Define an easing curve to be used when animating from this point (e.g. curve(1.0,0,0, 1.0))
 * or key words {standard | accelerate | decelerate | linear }
 * </td>
 * </tr>
 * <tr>
 * <td>transitionPathRotate</td>
 * <td>(float) rotate object relative to path taken</td>
 * </tr>
 * <tr>
 * <td>drawPath</td>
 * <td>draw the path the layout will animate animate</td>
 * </tr>
 * <tr>
 * <td>motionProgress</td>
 * <td>call method setProgress(float) on this  view
 * (used to talk to nested ConstraintLayouts etc.)</td>
 * </tr>
 * <tr>
 * <td>[standard view attributes](except visibility)</td>
 * <td>A collection of post layout view attributes see below </td>
 * </tr>
 * <tr>
 * <p>
 * <tr>
 * <td>{@code <CustomAttribute> }</td>
 * <td>call a set"name" method via reflection</td>
 * </tr>
 * </table>
 *
 * <h2>CustomAttribute</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>attributeName</td>
 * <td>The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)</td>
 * </tr>
 * <tr>
 * <td>customColorValue</td>
 * <td>The value is a color looking setMyAttr(int )</td>
 * </tr>
 * <tr>
 * <td>customIntegerValue</td>
 * <td>The value is an integer looking setMyAttr(int )</td>
 * </tr>
 * <tr>
 * <td>customFloatValue</td>
 * <td>The value is a float looking setMyAttr(float )</td>
 * </tr>
 * <tr>
 * <td>customStringValue</td>
 * <td>The value is a String looking setMyAttr(String )</td>
 * </tr>
 * <tr>
 * <td>customDimension</td>
 * <td>The value is a dimension looking setMyAttr(float )</td>
 * </tr>
 * <tr>
 * <td>customBoolean</td>
 * <td>The value is true or false looking setMyAttr(boolean )</td>
 * </tr>
 * </table>
 *
 * </p>
 * <p>
 * <h2>KeyCycle</h2>
 * <table summary="Constraint attributes">
 * <tr>
 * <td>motionTarget</td>
 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td>
 * </tr>
 * <tr>
 * <td>framePosition</td>
 * <td>The point along the interpolation 0 = start 100 = end</td>
 * </tr>
 * <tr>
 * <td>[Standard View attributes]</td>
 * <td>A collection of view attributes supported by the system (see below)</td>
 * </tr>
 * <tr>
 * <td>waveShape</td>
 * <td>The shape of the wave to generate
 * {sin|square|triangle|sawtooth|reverseSawtooth|cos|bounce}</td>
 * </tr>
 * <tr>
 * <td>wavePeriod</td>
 * <td>The number of cycles to loop near this region</td>
 * </tr>
 * <tr>
 * <td>waveOffset</td>
 * <td>offset value added to the attribute</td>
 * </tr>
 * <tr>
 * <td>transitionPathRotate</td>
 * <td>Cycles applied to rotation relative to the path the view is travelling</td>
 * </tr>
 * <tr>
 * <td>progress</td>
 * <td>call method setProgress(float) on this  view
 * (used to talk to nested ConstraintLayouts etc.)</td>
 * </tr>
 * <tr>
 * <td>{@code <CustomAttribute> }</td>
 * <td>call a set"name" method via reflection (limited to floats)</td>
 * </tr>
 * </table>
 *
 * <h2>CustomAttribute</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>attributeName</td>
 * <td>The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)</td>
 * </tr>
 * <tr>
 * <tr>
 * <td>customFloatValue</td>
 * <td>The value is a float looking setMyAttr(float )</td>
 * </tr>
 * <tr>
 * </table>
 *
 * <h2>KeyTimeCycle</h2>
 * <table summary="Constraint attributes">
 * <tr>
 * <td>motionTarget</td>
 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td>
 * </tr>
 * <tr>
 * <td>framePosition</td>
 * <td>The point along the interpolation 0 = start 100 = end</td>
 * </tr>
 * <tr>
 * <td>[Standard View attributes]</td>
 * <td>A collection of view attributes supported by the system (see below)</td>
 * </tr>
 * <tr>
 * <td>waveShape</td>
 * <td>The shape of the wave to generate
 * {sin|square|triangle|sawtooth|reverseSawtooth|cos|bounce}</td>
 * </tr>
 * <tr>
 * <td>wavePeriod</td>
 * <td>The number of cycles per second</td>
 * </tr>
 * <tr>
 * <td>waveOffset</td>
 * <td>offset value added to the attribute</td>
 * </tr>
 * <tr>
 * <td>transitionPathRotate</td>
 * <td>Cycles applied to rotation relative to the path the view is travelling</td>
 * </tr>
 * <tr>
 * <td>progress</td>
 * <td>call method setProgress(float) on this  view
 * (used to talk to nested ConstraintLayouts etc.)</td>
 * </tr>
 * <tr>
 * <td>{@code <CustomAttribute> }</td>
 * <td>call a set"name" method via reflection (limited to floats)</td>
 * </tr>
 * </table>
 *
 * <h2>CustomAttribute</h2>
 * <table summary="Variant attributes" >
 * <tr>
 * <td>attributeName</td>
 * <td>The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)</td>
 * </tr>
 * <tr>
 * <tr>
 * <td>customFloatValue</td>
 * <td>The value is a float looking setMyAttr(float )</td>
 * </tr>
 * <tr>
 * </table>
 *
 * <h2>KeyTrigger</h2>
 * <table summary="KeyTrigger attributes">
 * <tr>
 * <td>motionTarget</td>
 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td>
 * </tr>
 * <tr>
 * <td>framePosition</td>
 * <td>The point along the interpolation 0 = start 100 = end</td>
 * </tr
 * <tr>
 * <td>onCross</td>
 * <td>(method name) on crossing this position call this methods on the t arget
 * </td>
 * </tr>
 * <tr>
 * <td>onPositiveCross</td>
 * <td>(method name) on forward crossing of the framePosition call this methods on the target</td>
 * </tr>
 * <tr>
 * <td>onNegativeCross/td>
 * <td>(method name) backward crossing of the framePosition call this methods on the target</td>
 * </tr>
 * <tr>
 * <td>viewTransitionOnCross</td>
 * <td>(ViewTransition Id) start a NoState view transition on crossing or hitting target
 * </td>
 * </tr>
 * <tr>
 * <td>viewTransitionOnPositiveCross</td>
 * <td>(ViewTransition Id) start a NoState view transition forward crossing of the
 * framePosition or entering target</td>
 * </tr>
 * <tr>
 * <td>viewTransitionOnNegativeCross/td>
 * <td>(ViewTransition Id) start a NoState view transition backward crossing of the
 * framePosition or leaving target</td>
 * </tr>
 * <tr>
 * <td>triggerSlack</td>
 * <td>(float) do not call trigger again if the framePosition has not moved this
 * fraction away from the trigger point</td>
 * </tr>
 * <tr>
 * <td>triggerId</td>
 * <td>(id) call the TransitionListener with this trigger id</td>
 * </tr>
 * <tr>
 * <td>motion_postLayoutCollision</td>
 * <td>Define motion pre or post layout. Post layout is more expensive but captures
 * KeyAttributes or KeyCycle motions.</td>
 * </tr>
 * <tr>
 * <td>motion_triggerOnCollision</td>
 * <td>(id) Trigger if the motionTarget collides with the other motionTarget</td>
 * </tr>
 * </table>
 *
 * </p>
 * <p>
 * <h2>Standard attributes</h2>
 * <table summary="Constraint attributes">
 * <tr>
 * <td>android:visibility</td>
 * <td>Android view attribute that</td>
 * </tr>
 * <tr>
 * <td>android:alpha</td>
 * <td>Android view attribute that</td>
 * </tr>
 * <tr>
 * <td>android:elevation</td>
 * <td>base z depth of the view.</td>
 * </tr>
 * <tr>
 * <td>android:rotation</td>
 * <td>rotation of the view, in degrees.</td>
 * </tr>
 * <tr>
 * <td>android:rotationX</td>
 * <td>rotation of the view around the x axis, in degrees.</td>
 * </tr>
 * <tr>
 * <td>android:rotationY</td>
 * <td>rotation of the view around the y axis, in degrees.</td>
 * </tr>
 * <tr>
 * <td>android:scaleX</td>
 * <td>scale of the view in the x direction.</td>
 * </tr>
 * <tr>
 * <td>android:scaleY</td>
 * <td>scale of the view in the y direction.</td>
 * </tr>
 * <tr>
 * <td>android:translationX</td>
 * <td>translation in x of the view.</td>
 * </tr>
 * <tr>
 * <td>android:translationY</td>
 * <td>translation in y of the view.</td>
 * </tr>
 * <tr>
 * <td>android:translationZ</td>
 * <td>translation in z of the view.</td>
 * </tr>
 * <p>
 * </table>
 *
 * </p>
 */
public class MotionLayout extends ConstraintLayout implements
        NestedScrollingParent3 {

    public static final int TOUCH_UP_COMPLETE = 0;
    public static final int TOUCH_UP_COMPLETE_TO_START = 1;
    public static final int TOUCH_UP_COMPLETE_TO_END = 2;
    public static final int TOUCH_UP_STOP = 3;
    public static final int TOUCH_UP_DECELERATE = 4;
    public static final int TOUCH_UP_DECELERATE_AND_COMPLETE = 5;
    public static final int TOUCH_UP_NEVER_TO_START = 6;
    public static final int TOUCH_UP_NEVER_TO_END = 7;

    static final String TAG = "MotionLayout";
    private static final boolean DEBUG = false;

    public static boolean IS_IN_EDIT_MODE;

    MotionScene mScene;
    Interpolator mInterpolator;
    Interpolator mProgressInterpolator = null;
    float mLastVelocity = 0;
    private int mBeginState = UNSET;
    int mCurrentState = UNSET;
    private int mEndState = UNSET;
    private int mLastWidthMeasureSpec = 0;
    private int mLastHeightMeasureSpec = 0;
    private boolean mInteractionEnabled = true;

    HashMap<View, MotionController> mFrameArrayList = new HashMap<>();

    private long mAnimationStartTime = 0;
    private float mTransitionDuration = 1f;
    float mTransitionPosition = 0.0f;
    float mTransitionLastPosition = 0.0f;
    private long mTransitionLastTime;
    float mTransitionGoalPosition = 0.0f;
    private boolean mTransitionInstantly;
    boolean mInTransition = false;
    boolean mIndirectTransition = false;
    private TransitionListener mTransitionListener;
    private float mLastPos;
    private float mLastY;
    public static final int DEBUG_SHOW_NONE = 0;
    public static final int DEBUG_SHOW_PROGRESS = 1;
    public static final int DEBUG_SHOW_PATH = 2;
    int mDebugPath = 0;
    // variable used in painting the debug
    static final int MAX_KEY_FRAMES = 50;
    DevModeDraw mDevModeDraw;
    private boolean mTemporalInterpolator = false;
    private StopLogic mStopLogic = new StopLogic();
    private DecelerateInterpolator mDecelerateLogic = new DecelerateInterpolator();

    private DesignTool mDesignTool;

    boolean mFirstDown = true;

    int mOldWidth;
    int mOldHeight;
    int mLastLayoutWidth;
    int mLastLayoutHeight;

    boolean mUndergoingMotion = false;
    float mScrollTargetDX;
    float mScrollTargetDY;
    long mScrollTargetTime;
    float mScrollTargetDT;
    private boolean mKeepAnimating = false;

    private ArrayList<MotionHelper> mOnShowHelpers = null;
    private ArrayList<MotionHelper> mOnHideHelpers = null;
    private ArrayList<MotionHelper> mDecoratorsHelpers = null;
    private CopyOnWriteArrayList<TransitionListener> mTransitionListeners = null;
    private int mFrames = 0;
    private long mLastDrawTime = -1;
    private float mLastFps = 0;
    private int mListenerState = 0;
    private float mListenerPosition = 0.0f;
    boolean mIsAnimating = false;

    public static final int VELOCITY_POST_LAYOUT = 0;
    public static final int VELOCITY_LAYOUT = 1;
    public static final int VELOCITY_STATIC_POST_LAYOUT = 2;
    public static final int VELOCITY_STATIC_LAYOUT = 3;

    protected boolean mMeasureDuringTransition = false;
    int mStartWrapWidth;
    int mStartWrapHeight;
    int mEndWrapWidth;
    int mEndWrapHeight;
    int mWidthMeasureMode;
    int mHeightMeasureMode;
    float mPostInterpolationPosition;
    private KeyCache mKeyCache = new KeyCache();
    private boolean mInLayout = false;
    private StateCache mStateCache;
    private Runnable mOnComplete = null;
    private int[] mScheduledTransitionTo = null;
    int mScheduledTransitions = 0;
    private boolean mInRotation = false;
    int mRotatMode = 0;
    HashMap<View, ViewState> mPreRotate = new HashMap<>();
    private int mPreRotateWidth;
    private int mPreRotateHeight;
    private int mPreviouseRotation;
    Rect mTempRect = new Rect();
    private boolean mDelayedApply = false;

    MotionController getMotionController(int mTouchAnchorId) {
        return mFrameArrayList.get(findViewById(mTouchAnchorId));
    }

    enum TransitionState {
        UNDEFINED,
        SETUP,
        MOVING,
        FINISHED;
    };

    TransitionState mTransitionState = TransitionState.UNDEFINED;
    private static final float EPSILON = 0.00001f;

    public MotionLayout(@NonNull Context context) {
        super(context);
        init(null);
    }

    public MotionLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public MotionLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    /**
     * Subclasses can override to define testClasses
     *
     * @return
     */
    protected long getNanoTime() {
        return System.nanoTime();
    }

    /**
     * Subclasses can override to build test frameworks
     *
     * @return
     */
    protected MotionTracker obtainVelocityTracker() {
        return MyTracker.obtain();
    }

    /**
     * Disable the transition based on transitionID
     * @param transitionID
     * @param enable
     */
    public void enableTransition(int transitionID, boolean enable) {
        MotionScene.Transition t = getTransition(transitionID);
        if (enable) {
            t.setEnabled(true);
            return;
        } else {
            if (t == mScene.mCurrentTransition) { // disabling current transition
                List<MotionScene.Transition> transitions =
                        mScene.getTransitionsWithState(mCurrentState);
                for (MotionScene.Transition transition : transitions) {
                    if (transition.isEnabled()) {
                        mScene.mCurrentTransition = transition;
                        break;
                    }
                }
            }
            t.setEnabled(false);
        }
    }

    /**
     * Subclasses can override to build test frameworks
     */
    protected interface MotionTracker {
        void recycle();

        void clear();

        void addMovement(MotionEvent event);

        void computeCurrentVelocity(int units);

        void computeCurrentVelocity(int units, float maxVelocity);

        float getXVelocity();

        float getYVelocity();

        float getXVelocity(int id);

        float getYVelocity(int id);
    }

    void setState(TransitionState newState) {
        if (DEBUG) {
            Debug.logStack(TAG, mTransitionState + " -> " + newState + " "
                    + Debug.getName(getContext(), mCurrentState), 2);
        }
        if (newState == TransitionState.FINISHED && mCurrentState == UNSET) {
            return;
        }
        TransitionState oldState = mTransitionState;
        mTransitionState = newState;

        if (oldState == TransitionState.MOVING && newState == TransitionState.MOVING) {
            fireTransitionChange();
        }
        switch (oldState) {
            case UNDEFINED:
            case SETUP:
                if (newState == TransitionState.MOVING) {
                    fireTransitionChange();
                }
                if (newState == TransitionState.FINISHED) {
                    fireTransitionCompleted();
                }
                break;
            case MOVING:
                if (newState == TransitionState.FINISHED) {
                    fireTransitionCompleted();
                }
                break;
            case FINISHED:
                break;
        }
    }

    private static class MyTracker implements MotionTracker {
        VelocityTracker mTracker;
        private static MyTracker sMe = new MyTracker();

        public static MyTracker obtain() {
            sMe.mTracker = VelocityTracker.obtain();
            return sMe;
        }

        @Override
        public void recycle() {
            if (mTracker != null) {
                mTracker.recycle();
                mTracker = null; // not allowed to call after recycle
            }
        }

        @Override
        public void clear() {
            if (mTracker != null) {
                mTracker.clear();
            }
        }

        @Override
        public void addMovement(MotionEvent event) {
            if (mTracker != null) {
                mTracker.addMovement(event);
            }
        }

        @Override
        public void computeCurrentVelocity(int units) {
            if (mTracker != null) {
                mTracker.computeCurrentVelocity(units);
            }
        }

        @Override
        public void computeCurrentVelocity(int units, float maxVelocity) {
            if (mTracker != null) {
                mTracker.computeCurrentVelocity(units, maxVelocity);
            }
        }

        @Override
        public float getXVelocity() {
            if (mTracker != null) {
                return mTracker.getXVelocity();
            }
            return 0;
        }

        @Override
        public float getYVelocity() {
            if (mTracker != null) {
                return mTracker.getYVelocity();
            }
            return 0;
        }

        @Override
        public float getXVelocity(int id) {
            if (mTracker != null) {
                return mTracker.getXVelocity(id);
            }
            return 0;
        }

        @Override
        public float getYVelocity(int id) {
            if (mTracker != null) {
                return getYVelocity(id);
            }
            return 0;
        }
    }

    /**
     * sets the state to start in. To be used during OnCreate
     *
     * @param beginId the id of the start constraint set
     */
    void setStartState(int beginId) {
        if (!isAttachedToWindow()) {
            if (mStateCache == null) {
                mStateCache = new StateCache();
            }
            mStateCache.setStartState(beginId);
            mStateCache.setEndState(beginId);
            return;
        }
        mCurrentState = beginId;
    }

    /**
     * Set a transition explicitly between two constraint sets
     *
     * @param beginId the id of the start constraint set
     * @param endId   the id of the end constraint set
     */
    public void setTransition(int beginId, int endId) {
        if (!isAttachedToWindow()) {
            if (mStateCache == null) {
                mStateCache = new StateCache();
            }
            mStateCache.setStartState(beginId);
            mStateCache.setEndState(endId);
            return;
        }

        if (mScene != null) {
            mBeginState = beginId;
            mEndState = endId;
            if (DEBUG) {
                Log.v(TAG, Debug.getLocation() + " setTransition "
                        + Debug.getName(getContext(), beginId) + " -> "
                        + Debug.getName(getContext(), endId));
            }
            mScene.setTransition(beginId, endId);
            mModel.initFrom(mLayoutWidget, mScene.getConstraintSet(beginId),
                    mScene.getConstraintSet(endId));
            rebuildScene();
            mTransitionLastPosition = 0;
            transitionToStart();
        }
    }

    /**
     * Set a transition explicitly to a Transition that has an ID
     * The transition must have been named with android:id=...
     *
     * @param transitionId the id to set
     */
    public void setTransition(int transitionId) {
        if (mScene != null) {
            MotionScene.Transition transition = getTransition(transitionId);
            mBeginState = transition.getStartConstraintSetId();
            mEndState = transition.getEndConstraintSetId();

            if (!isAttachedToWindow()) {
                if (mStateCache == null) {
                    mStateCache = new StateCache();
                }
                mStateCache.setStartState(mBeginState);
                mStateCache.setEndState(mEndState);
                return;
            }

            if (DEBUG) {
                Log.v(TAG, Debug.getLocation() + " setTransition "
                        + Debug.getName(getContext(), mBeginState) + " -> "
                        + Debug.getName(getContext(), mEndState)
                        + "   current=" + Debug.getName(getContext(), mCurrentState));
            }

            float pos = Float.NaN;
            if (mCurrentState == mBeginState) {
                pos = 0;
            } else if (mCurrentState == mEndState) {
                pos = 1;
            }
            mScene.setTransition(transition);
            mModel.initFrom(mLayoutWidget,
                    mScene.getConstraintSet(mBeginState),
                    mScene.getConstraintSet(mEndState));
            rebuildScene();

            if (mTransitionLastPosition != pos) {
                // If the last drawn position isn't the same,
                // we might have to make sure we apply the corresponding constraintset.
                if (pos == 0) {
                    endTrigger(true);
                    mScene.getConstraintSet(mBeginState).applyTo(this);
                } else if (pos == 1) {
                    endTrigger(false);
                    mScene.getConstraintSet(mEndState).applyTo(this);
                }
            }

            mTransitionLastPosition = Float.isNaN(pos) ? 0 : pos;

            if (Float.isNaN(pos)) {
                Log.v(TAG, Debug.getLocation() + " transitionToStart ");
                transitionToStart();
            } else {
                setProgress(pos);
            }
        }
    }

    protected void setTransition(MotionScene.Transition transition) {
        mScene.setTransition(transition);
        setState(TransitionState.SETUP);
        if (mCurrentState == mScene.getEndId()) {
            mTransitionLastPosition = 1.0f;
            mTransitionPosition = 1.0f;
            mTransitionGoalPosition = 1;
        } else {
            mTransitionLastPosition = 0;
            mTransitionPosition = 0f;
            mTransitionGoalPosition = 0;
        }
        mTransitionLastTime =
                transition.isTransitionFlag(TRANSITION_FLAG_FIRST_DRAW) ? -1 : getNanoTime();
        if (DEBUG) {
            Log.v(TAG, Debug.getLocation() + "  new mTransitionLastPosition = "
                    + mTransitionLastPosition + "");
            Log.v(TAG, Debug.getLocation() + " setTransition was "
                    + Debug.getName(getContext(), mBeginState)
                    + " -> " + Debug.getName(getContext(), mEndState));
        }
        int newBeginState = mScene.getStartId();
        int newEndState = mScene.getEndId();
        if (newBeginState == mBeginState && newEndState == mEndState) {
            return;
        }
        mBeginState = newBeginState;
        mEndState = newEndState;
        mScene.setTransition(mBeginState, mEndState);

        if (DEBUG) {
            Log.v(TAG, Debug.getLocation() + " setTransition now "
                    + Debug.getName(getContext(), mBeginState) + " -> "
                    + Debug.getName(getContext(), mEndState));
        }

        mModel.initFrom(mLayoutWidget,
                mScene.getConstraintSet(mBeginState),
                mScene.getConstraintSet(mEndState));
        mModel.setMeasuredId(mBeginState, mEndState);
        mModel.reEvaluateState();

        rebuildScene();
    }

    /**
     * This overrides ConstraintLayout and only accepts a MotionScene.
     *
     * @param motionScene The resource id, or 0 to reset the MotionScene.
     */
    @Override
    public void loadLayoutDescription(int motionScene) {
        if (motionScene != 0) {
            try {
                mScene = new MotionScene(getContext(), this, motionScene);
                if (mCurrentState == UNSET && mScene != null) {
                    mCurrentState = mScene.getStartId();
                    mBeginState = mScene.getStartId();
                    mEndState = mScene.getEndId();
                }
                if (isAttachedToWindow()) {
                    try {
                        Display display = getDisplay();
                        mPreviouseRotation = (display == null) ? 0 : display.getRotation();

                        if (mScene != null) {
                            ConstraintSet cSet = mScene.getConstraintSet(mCurrentState);
                            mScene.readFallback(this);
                            if (mDecoratorsHelpers != null) {
                                for (MotionHelper mh : mDecoratorsHelpers) {
                                    mh.onFinishedMotionScene(this);
                                }
                            }
                            if (cSet != null) {
                                cSet.applyTo(this);
                            }
                            mBeginState = mCurrentState;
                        }
                        onNewStateAttachHandlers();
                        if (mStateCache != null) {
                            if (mDelayedApply) {
                                post(new Runnable() {
                                    @Override
                                    public void run() {
                                        mStateCache.apply();
                                    }
                                });
                            } else {
                                mStateCache.apply();
                            }
                        } else {
                            if (mScene != null && mScene.mCurrentTransition != null) {
                                if (mScene.mCurrentTransition.getAutoTransition()
                                        == MotionScene.Transition.AUTO_ANIMATE_TO_END) {
                                    transitionToEnd();
                                    setState(TransitionState.SETUP);
                                    setState(TransitionState.MOVING);
                                }
                            }

                        }
                    } catch (Exception ex) {
                        throw new IllegalArgumentException("unable to parse MotionScene file", ex);
                    }
                } else {
                    mScene = null;
                }

            } catch (Exception ex) {
                throw new IllegalArgumentException("unable to parse MotionScene file", ex);
            }
        } else {
            mScene = null;
        }
    }

    /**
     * Set the State of the Constraint layout. Causing it to load a particular ConstraintSet.
     * for states with variants the variant with matching
     * width and height constraintSet will be chosen
     *
     * @param id           set the state width and height
     * @param screenWidth
     * @param screenHeight
     */
    @Override
    public void setState(int id, int screenWidth, int screenHeight) {
        setState(TransitionState.SETUP);
        mCurrentState = id;
        mBeginState = UNSET;
        mEndState = UNSET;
        if (mConstraintLayoutSpec != null) {
            mConstraintLayoutSpec.updateConstraints(id, screenWidth, screenHeight);
        } else if (mScene != null) {
            mScene.getConstraintSet(id).applyTo(this);
        }
    }

    /**
     * Set the transition position between 0 an 1
     *
     * @param pos
     */
    public void setInterpolatedProgress(float pos) {
        if (mScene != null) {
            setState(TransitionState.MOVING);
            Interpolator interpolator = mScene.getInterpolator();
            if (interpolator != null) {
                setProgress(interpolator.getInterpolation(pos));
                return;
            }
        }
        setProgress(pos);
    }

    /**
     * Set the transition position between 0 an 1
     *
     * @param pos
     * @param velocity
     */
    public void setProgress(float pos, float velocity) {
        if (!isAttachedToWindow()) {
            if (mStateCache == null) {
                mStateCache = new StateCache();
            }
            mStateCache.setProgress(pos);
            mStateCache.setVelocity(velocity);
            return;
        }
        setProgress(pos);
        setState(TransitionState.MOVING);
        mLastVelocity = velocity;
        if (velocity != 0.0f) {
            animateTo(velocity > 0 ? 1 : 0);
        } else if (pos != 0f && pos != 1f) {
            animateTo(pos > 0.5f ? 1 : 0);
        }
    }

    /////////////////////// use to cache the state
    class StateCache {
        float mProgress = Float.NaN;
        float mVelocity = Float.NaN;
        int mStartState = UNSET;
        int mEndState = UNSET;
        final String mKeyProgress = "motion.progress";
        final String mKeyVelocity = "motion.velocity";
        final String mKeyStartState = "motion.StartState";
        final String mKeyEndState = "motion.EndState";

        void apply() {
            if (this.mStartState != UNSET || this.mEndState != UNSET) {
                if (this.mStartState == UNSET) {
                    transitionToState(mEndState);
                } else if (this.mEndState == UNSET) {
                    setState(this.mStartState, -1, -1);
                } else {
                    setTransition(mStartState, mEndState);
                }
                setState(TransitionState.SETUP);
            }
            if (Float.isNaN(this.mVelocity)) {
                if (Float.isNaN(this.mProgress)) {
                    return;
                }
                MotionLayout.this.setProgress(this.mProgress);
                return;
            }
            MotionLayout.this.setProgress(this.mProgress, mVelocity);
            this.mProgress = Float.NaN;
            this.mVelocity = Float.NaN;
            this.mStartState = UNSET;
            this.mEndState = UNSET;
        }

        public Bundle getTransitionState() {
            Bundle bundle = new Bundle();
            bundle.putFloat(mKeyProgress, this.mProgress);
            bundle.putFloat(mKeyVelocity, this.mVelocity);
            bundle.putInt(mKeyStartState, this.mStartState);
            bundle.putInt(mKeyEndState, this.mEndState);
            return bundle;
        }

        public void setTransitionState(Bundle bundle) {
            this.mProgress = bundle.getFloat(mKeyProgress);
            this.mVelocity = bundle.getFloat(mKeyVelocity);
            this.mStartState = bundle.getInt(mKeyStartState);
            this.mEndState = bundle.getInt(mKeyEndState);
        }

        public void setProgress(float progress) {
            this.mProgress = progress;
        }

        public void setEndState(int endState) {
            this.mEndState = endState;
        }

        public void setVelocity(float mVelocity) {
            this.mVelocity = mVelocity;
        }

        public void setStartState(int startState) {
            this.mStartState = startState;
        }

        public void recordState() {
            mEndState = MotionLayout.this.mEndState;
            mStartState = MotionLayout.this.mBeginState;
            mVelocity = MotionLayout.this.getVelocity();
            mProgress = MotionLayout.this.getProgress();
        }
    }

    /**
     * Set the transition state as a bundle
     */
    public void setTransitionState(Bundle bundle) {
        if (mStateCache == null) {
            mStateCache = new StateCache();
        }
        mStateCache.setTransitionState(bundle);
        if (isAttachedToWindow()) {
            mStateCache.apply();
        }
    }

    /**
     * @return bundle containing start and end state
     */
    public Bundle getTransitionState() {
        if (mStateCache == null) {
            mStateCache = new StateCache();
        }
        mStateCache.recordState();
        return mStateCache.getTransitionState();
    }

    /**
     * Set the transition position between 0 an 1
     *
     * @param pos the position in the transition from 0...1
     */
    public void setProgress(float pos) {
        if (pos < 0.0f || pos > 1.0f) {
            Log.w(TAG, "Warning! Progress is defined for values between 0.0 and 1.0 inclusive");
        }
        if (!isAttachedToWindow()) {
            if (mStateCache == null) {
                mStateCache = new StateCache();
            }
            mStateCache.setProgress(pos);
            return;
        }
        if (DEBUG) {
            String str = getContext().getResources().getResourceName(mBeginState) + " -> ";
            str += getContext().getResources().getResourceName(mEndState) + ":" + getProgress();
            Log.v(TAG, Debug.getLocation() + " > " + str);
            Debug.logStack(TAG, " Progress = " + pos, 3);
        }

        if (pos <= 0f) {
            if (mTransitionLastPosition == 1.0f && mCurrentState == mEndState) {
                setState(TransitionState.MOVING); // fire a transient moving as jumping start to end
            }

            mCurrentState = mBeginState;
            if (mTransitionLastPosition == 0.0f) {
                setState(TransitionState.FINISHED);
            }
        } else if (pos >= 1.0f) {
            if (mTransitionLastPosition == 0.0f && mCurrentState == mBeginState) {
                setState(TransitionState.MOVING); // fire a transient moving as jumping end to start
            }

            mCurrentState = mEndState;
            if (mTransitionLastPosition == 1.0f) {
                setState(TransitionState.FINISHED);
            }
        } else {
            mCurrentState = UNSET;
            setState(TransitionState.MOVING);
        }

        if (mScene == null) {
            return;
        }

        mTransitionInstantly = true;
        mTransitionGoalPosition = pos;
        mTransitionPosition = pos;
        mTransitionLastTime = -1;
        mAnimationStartTime = -1;
        mInterpolator = null;

        mInTransition = true;
        invalidate();
    }

    /**
     * Create a transition view for every view
     */
    private void setupMotionViews() {
        int n = getChildCount();

        mModel.build();
        mInTransition = true;
        SparseArray<MotionController> controllers = new SparseArray<>();
        for (int i = 0; i < n; i++) {
            View child = getChildAt(i);
            controllers.put(child.getId(), mFrameArrayList.get(child));
        }
        int layoutWidth = getWidth();
        int layoutHeight = getHeight();
        int arc = mScene.gatPathMotionArc();
        if (arc != UNSET) {
            for (int i = 0; i < n; i++) {
                MotionController motionController = mFrameArrayList.get(getChildAt(i));
                if (motionController != null) {
                    motionController.setPathMotionArc(arc);
                }
            }
        }

        SparseBooleanArray sparseBooleanArray = new SparseBooleanArray();
        int[] depends = new int[mFrameArrayList.size()];
        int count = 0;
        for (int i = 0; i < n; i++) {
            View view = getChildAt(i);
            MotionController motionController = mFrameArrayList.get(view);
            if (motionController.getAnimateRelativeTo() != UNSET) {
                sparseBooleanArray.put(motionController.getAnimateRelativeTo(), true);
                depends[count++] = motionController.getAnimateRelativeTo();
            }
        }
        if (mDecoratorsHelpers != null) {
            for (int i = 0; i < count; i++) {
                MotionController motionController = mFrameArrayList.get(findViewById(depends[i]));
                if (motionController == null) {
                    continue;
                }
                mScene.getKeyFrames(motionController);
            }
            // Allow helpers to access all the motionControllers after
            for (MotionHelper mDecoratorsHelper : mDecoratorsHelpers) {
                mDecoratorsHelper.onPreSetup(this, mFrameArrayList);
            }
            for (int i = 0; i < count; i++) {
                MotionController motionController = mFrameArrayList.get(findViewById(depends[i]));
                if (motionController == null) {
                    continue;
                }
                motionController.setup(layoutWidth, layoutHeight,
                        mTransitionDuration, getNanoTime());
            }
        } else {
            for (int i = 0; i < count; i++) {
                MotionController motionController = mFrameArrayList.get(findViewById(depends[i]));
                if (motionController == null) {
                    continue;
                }
                mScene.getKeyFrames(motionController);
                motionController.setup(layoutWidth, layoutHeight,
                        mTransitionDuration, getNanoTime());
            }
        }
        // getMap the KeyFrames for each view
        for (int i = 0; i < n; i++) {
            View v = getChildAt(i);
            MotionController motionController = mFrameArrayList.get(v);
            if (sparseBooleanArray.get(v.getId())) {
                continue;
            }

            if (motionController != null) {
                mScene.getKeyFrames(motionController);
                motionController.setup(layoutWidth, layoutHeight,
                        mTransitionDuration, getNanoTime());
            }
        }

        float stagger = mScene.getStaggered();
        if (stagger != 0.0f) {
            boolean flip = stagger < 0.0;
            boolean useMotionStagger = false;
            stagger = Math.abs(stagger);
            float min = Float.MAX_VALUE, max = -Float.MAX_VALUE;
            for (int i = 0; i < n; i++) {
                MotionController f = mFrameArrayList.get(getChildAt(i));
                if (!Float.isNaN(f.mMotionStagger)) {
                    useMotionStagger = true;
                    break;
                }
                float x = f.getFinalX();
                float y = f.getFinalY();
                float mdist = flip ? (y - x) : (y + x);
                min = Math.min(min, mdist);
                max = Math.max(max, mdist);
            }
            if (useMotionStagger) {
                min = Float.MAX_VALUE;
                max = -Float.MAX_VALUE;

                for (int i = 0; i < n; i++) {
                    MotionController f = mFrameArrayList.get(getChildAt(i));
                    if (!Float.isNaN(f.mMotionStagger)) {
                        min = Math.min(min, f.mMotionStagger);
                        max = Math.max(max, f.mMotionStagger);
                    }
                }
                for (int i = 0; i < n; i++) {
                    MotionController f = mFrameArrayList.get(getChildAt(i));
                    if (!Float.isNaN(f.mMotionStagger)) {

                        f.mStaggerScale = 1 / (1 - stagger);
                        if (flip) {
                            f.mStaggerOffset = stagger - stagger
                                    * ((max - f.mMotionStagger) / (max - min));
                        } else {
                            f.mStaggerOffset = stagger - stagger
                                    * (f.mMotionStagger - min) / (max - min);
                        }
                    }
                }
            } else {
                for (int i = 0; i < n; i++) {
                    MotionController f = mFrameArrayList.get(getChildAt(i));
                    float x = f.getFinalX();
                    float y = f.getFinalY();
                    float mdist = flip ? (y - x) : (y + x);
                    f.mStaggerScale = 1 / (1 - stagger);
                    f.mStaggerOffset = stagger - stagger * (mdist - min) / (max - min);
                }
            }
        }
    }

    /**
     * @param touchUpMode     behavior on touch up, can be either:
     *                        <ul>
     *                        <li>TOUCH_UP_COMPLETE (default) : will complete the transition,
     *                        picking up
     *                        automatically a correct velocity to do so</li>
     *                        <li>TOUCH_UP_STOP : will allow stopping mid-transition</li>
     *                        <li>TOUCH_UP_DECELERATE : will slowly decay,
     *                        possibly past the transition (i.e.
     *                        it will do a hard stop if unmanaged)</li>
     *                        <li>TOUCH_UP_DECELERATE_AND_COMPLETE :
     *                        will automatically pick between
     *                        TOUCH_UP_COMPLETE and TOUCH_UP_DECELERATE</li>
     *                        </ul>,
     *                        TOUCH_UP_STOP (will allow stopping
     * @param position        animate to given position
     * @param currentVelocity
     */
    public void touchAnimateTo(int touchUpMode, float position, float currentVelocity) {
        if (DEBUG) {
            Log.v(TAG, " " + Debug.getLocation() + " touchAnimateTo "
                    + position + "   " + currentVelocity);
        }
        if (mScene == null) {
            return;
        }
        if (mTransitionLastPosition == position) {
            return;
        }

        mTemporalInterpolator = true;
        mAnimationStartTime = getNanoTime();
        mTransitionDuration = mScene.getDuration() / 1000f;

        mTransitionGoalPosition = position;
        mInTransition = true;

        switch (touchUpMode) {
            case TOUCH_UP_COMPLETE:
            case TOUCH_UP_NEVER_TO_START:
            case TOUCH_UP_NEVER_TO_END:
            case TOUCH_UP_COMPLETE_TO_START:
            case TOUCH_UP_COMPLETE_TO_END: {
                if (touchUpMode == TOUCH_UP_COMPLETE_TO_START
                        || touchUpMode == TOUCH_UP_NEVER_TO_END) {
                    position = 0;
                } else if (touchUpMode == TOUCH_UP_COMPLETE_TO_END
                        || touchUpMode == TOUCH_UP_NEVER_TO_START) {
                    position = 1;
                }

                if (mScene.getAutoCompleteMode()
                        == TouchResponse.COMPLETE_MODE_CONTINUOUS_VELOCITY) {
                    mStopLogic.config(mTransitionLastPosition, position, currentVelocity,
                            mTransitionDuration, mScene.getMaxAcceleration(),
                            mScene.getMaxVelocity());
                } else {
                    mStopLogic.springConfig(mTransitionLastPosition, position, currentVelocity,
                            mScene.getSpringMass(),
                            mScene.getSpringStiffiness(),
                            mScene.getSpringDamping(),
                            mScene.getSpringStopThreshold(), mScene.getSpringBoundary());
                }

                int currentState = mCurrentState; // TODO: remove setProgress(), temporary fix
                mTransitionGoalPosition = position;
                mCurrentState = currentState;
                mInterpolator = mStopLogic;
            }
                break;
            case TOUCH_UP_STOP: {
                // nothing to do
            }
            break;
            case TOUCH_UP_DECELERATE: {
                mDecelerateLogic.config(currentVelocity, mTransitionLastPosition,
                        mScene.getMaxAcceleration());
                mInterpolator = mDecelerateLogic;
            }
            break;
            case TOUCH_UP_DECELERATE_AND_COMPLETE: {
                if (willJump(currentVelocity, mTransitionLastPosition,
                        mScene.getMaxAcceleration())) {
                    mDecelerateLogic.config(currentVelocity,
                            mTransitionLastPosition, mScene.getMaxAcceleration());
                    mInterpolator = mDecelerateLogic;
                } else {
                    mStopLogic.config(mTransitionLastPosition, position, currentVelocity,
                            mTransitionDuration,
                            mScene.getMaxAcceleration(), mScene.getMaxVelocity());
                    mLastVelocity = 0;
                    int currentState = mCurrentState; // TODO: remove setProgress(), (temporary fix)
                    mTransitionGoalPosition = position;
                    mCurrentState = currentState;
                    mInterpolator = mStopLogic;
                }
            }
            break;
        }

        mTransitionInstantly = false;
        mAnimationStartTime = getNanoTime();
        invalidate();
    }

    /**
     * Allows you to use trigger spring motion touch behaviour.
     * You must have configured all the spring parameters in the Transition's OnSwipe
     *
     * @param position the position 0 - 1
     * @param currentVelocity the current velocity rate of change in position per second
     */
    public void touchSpringTo(float position, float currentVelocity) {
        if (DEBUG) {
            Log.v(TAG, " " + Debug.getLocation()
                    + " touchAnimateTo " + position + "   " + currentVelocity);
        }
        if (mScene == null) {
            return;
        }
        if (mTransitionLastPosition == position) {
            return;
        }

        mTemporalInterpolator = true;
        mAnimationStartTime = getNanoTime();
        mTransitionDuration = mScene.getDuration() / 1000f;

        mTransitionGoalPosition = position;
        mInTransition = true;

        mStopLogic.springConfig(mTransitionLastPosition, position, currentVelocity,
                mScene.getSpringMass(), mScene.getSpringStiffiness(), mScene.getSpringDamping(),
                mScene.getSpringStopThreshold(), mScene.getSpringBoundary());

        int currentState = mCurrentState; // TODO: remove setProgress(), this is a temporary fix
        mTransitionGoalPosition = position;
        mCurrentState = currentState;
        mInterpolator = mStopLogic;


        mTransitionInstantly = false;
        mAnimationStartTime = getNanoTime();
        invalidate();
    }

    private static boolean willJump(float velocity,
                                    float position,
                                    float maxAcceleration) {
        if (velocity > 0) {
            float time = velocity / maxAcceleration;
            float pos = velocity * time - (maxAcceleration * time * time) / 2;
            return (position + pos > 1);
        } else {
            float time = -velocity / maxAcceleration;
            float pos = velocity * time + (maxAcceleration * time * time) / 2;
            return (position + pos < 0);
        }
    }

    /**
     * Basic deceleration interpolator
     */
    class DecelerateInterpolator extends MotionInterpolator {
        float mInitialV = 0;
        float mCurrentP = 0;
        float mMaxA;

        public void config(float velocity, float position, float maxAcceleration) {
            mInitialV = velocity;
            mCurrentP = position;
            mMaxA = maxAcceleration;
        }

        @Override
        public float getInterpolation(float time) {
            if (mInitialV > 0) {
                if (mInitialV / mMaxA < time) {
                    time = mInitialV / mMaxA;
                }
                mLastVelocity = mInitialV - mMaxA * time;
                float pos = mInitialV * time - (mMaxA * time * time) / 2;
                return pos + mCurrentP;
            } else {

                if (-mInitialV / mMaxA < time) {
                    time = -mInitialV / mMaxA;
                }
                mLastVelocity = mInitialV + mMaxA * time;
                float pos = mInitialV * time + (mMaxA * time * time) / 2;
                return pos + mCurrentP;
            }
        }

        @Override
        public float getVelocity() {
            return mLastVelocity;
        }
    }

    /**
     * @param position animate to given position
     */
    void animateTo(float position) {
        if (DEBUG) {
            Log.v(TAG, " " + Debug.getLocation() + " ... animateTo(" + position
                    + ") last:" + mTransitionLastPosition);
        }
        if (mScene == null) {
            return;
        }

        if (mTransitionLastPosition != mTransitionPosition && mTransitionInstantly) {
            // if we had a call from setProgress() but evaluate() didn't run,
            // the mTransitionLastPosition might not have been updated
            mTransitionLastPosition = mTransitionPosition;
        }

        if (mTransitionLastPosition == position) {
            return;
        }
        mTemporalInterpolator = false;
        float currentPosition = mTransitionLastPosition;
        mTransitionGoalPosition = position;
        mTransitionDuration = mScene.getDuration() / 1000f;
        setProgress(mTransitionGoalPosition);
        mInterpolator = null;
        mProgressInterpolator = mScene.getInterpolator();
        mTransitionInstantly = false;
        mAnimationStartTime = getNanoTime();
        mInTransition = true;
        mTransitionPosition = currentPosition;
        if (DEBUG) {
            Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = "
                    + mTransitionLastPosition + " currentPosition =" + currentPosition);
        }
        mTransitionLastPosition = currentPosition;
        invalidate();
    }

    private void computeCurrentPositions() {
        final int n = getChildCount();
        for (int i = 0; i < n; i++) {
            View v = getChildAt(i);
            MotionController frame = mFrameArrayList.get(v);
            if (frame == null) {
                continue;
            }
            frame.setStartCurrentState(v);
        }
    }

    /**
     * Animate to the starting position of the current transition.
     * This will not work during on create as there is no transition
     * Transitions are only set up during onAttach
     */
    public void transitionToStart() {
        animateTo(0.0f);
    }

    /**
     * Animate to the starting position of the current transition.
     * This will not work during on create as there is no transition
     * Transitions are only set up during onAttach
     *
     * @param onComplete callback when task is done
     */
    public void transitionToStart(Runnable onComplete) {
        animateTo(0.0f);
        mOnComplete = onComplete;
    }

    /**
     * Animate to the ending position of the current transition.
     * This will not work during on create as there is no transition
     * Transitions are only set up during onAttach
     */
    public void transitionToEnd() {
        animateTo(1.0f);
        mOnComplete = null;
    }

    /**
     * Animate to the ending position of the current transition.
     * This will not work during on create as there is no transition
     * Transitions are only set up during onAttach
     *
     * @param onComplete callback when task is done
     */
    public void transitionToEnd(Runnable onComplete) {
        animateTo(1.0f);
        mOnComplete = onComplete;
    }

    /**
     * Animate to the state defined by the id.
     * The id is the id of the ConstraintSet or the id of the State.
     *
     * @param id the state to transition to
     */
    public void transitionToState(int id) {
        if (!isAttachedToWindow()) {
            if (mStateCache == null) {
                mStateCache = new StateCache();
            }
            mStateCache.setEndState(id);
            return;
        }
        transitionToState(id, -1, -1);
    }

    /**
     * Animate to the state defined by the id.
     * The id is the id of the ConstraintSet or the id of the State.
     *
     * @param id       the state to transition to
     * @param duration time in ms. if 0 set by default or transition -1 by current
     */

    public void transitionToState(int id, int duration) {
        if (!isAttachedToWindow()) {
            if (mStateCache == null) {
                mStateCache = new StateCache();
            }
            mStateCache.setEndState(id);
            return;
        }
        transitionToState(id, -1, -1, duration);
    }

    /**
     * Animate to the state defined by the id.
     * Width and height may be used in the picking of the id using this StateSet.
     *
     * @param id           the state to transition
     * @param screenWidth  the with of the motionLayout used to select the variant
     * @param screenHeight the height of the motionLayout used to select the variant
     */
    public void transitionToState(int id, int screenWidth, int screenHeight) {
        transitionToState(id, screenWidth, screenHeight, -1);
    }

    /**
     * Rotate the layout based on the angle to a ConstraintSet
     * @param id constraintSet
     * @param duration time to take to rotate
     */
    public void rotateTo(int id, int duration) {
        mInRotation = true;
        mPreRotateWidth = getWidth();
        mPreRotateHeight = getHeight();

        int currentRotation = getDisplay().getRotation();
        mRotatMode = (((currentRotation + 1) % 4) > ((mPreviouseRotation + 1) % 4)) ? 1 : 2;

        mPreviouseRotation = currentRotation;
        final int n = getChildCount();
        for (int i = 0; i < n; i++) {
            View v = getChildAt(i);
            ViewState bounds = mPreRotate.get(v);
            if (bounds == null) {
                bounds = new ViewState();
                mPreRotate.put(v, bounds);
            }
            bounds.getState(v);
        }

        mBeginState = -1;
        mEndState = id;
        mScene.setTransition(-1, mEndState);
        mModel.initFrom(mLayoutWidget, null, mScene.getConstraintSet(mEndState));
        mTransitionPosition = 0;

        mTransitionLastPosition = 0;
        invalidate();
        transitionToEnd(new Runnable() {
            @Override
            public void run() {
                mInRotation = false;
            }
        });
        if (duration > 0) {
            mTransitionDuration = duration / 1000f;
        }

    }

    public boolean isInRotation() {
        return mInRotation;
    }

    /**
     * This jumps to a state
     * It will be at that state after one repaint cycle
     * If the current transition contains that state.
     * It setsProgress 0 or 1 to that state.
     * If not in the current transition itsl
     *
     * @param id state to set
     */
    public void jumpToState(int id) {
        if (!isAttachedToWindow()) {
            mCurrentState = id;
        }
        if (mBeginState == id) {
            setProgress(0);
        } else if (mEndState == id) {
            setProgress(1);
        } else {
            setTransition(id, id);
        }
    }

    /**
     * Animate to the state defined by the id.
     * Width and height may be used in the picking of the id using this StateSet.
     *
     * @param id           the state to transition
     * @param screenWidth  the with of the motionLayout used to select the variant
     * @param screenHeight the height of the motionLayout used to select the variant
     * @param duration     time in ms. if 0 set by default or transition -1 by current
     */

    public void transitionToState(int id, int screenWidth, int screenHeight, int duration) {
        // if id is either end or start state, transition using current setup.
        // if id is not part of end/start, need to setup

        // if id == end state, just animate
        // ... but check if currentState is unknown. if unknown, call computeCurrentPosition
        // if id != end state
        if (DEBUG && mScene.mStateSet == null) {
            Log.v(TAG, Debug.getLocation() + " mStateSet = null");
        }
        if (mScene != null && mScene.mStateSet != null) {
            int tmp_id = mScene.mStateSet.convertToConstraintSet(mCurrentState,
                    id, screenWidth, screenHeight);

            if (tmp_id != -1) {
                if (DEBUG) {
                    Log.v(TAG, " got state  " + Debug.getLocation() + " lookup("
                            + Debug.getName(getContext(), id)
                            + screenWidth + " , " + screenHeight + " ) =  "
                            + Debug.getName(getContext(), tmp_id));
                }
                id = tmp_id;
            }
        }
        if (mCurrentState == id) {
            return;
        }
        if (mBeginState == id) {
            animateTo(0.0f);
            if (duration > 0) {
                mTransitionDuration = duration / 1000f;
            }
            return;
        }
        if (mEndState == id) {
            animateTo(1.0f);
            if (duration > 0) {
                mTransitionDuration = duration / 1000f;
            }
            return;
        }
        mEndState = id;
        if (mCurrentState != UNSET) {
            if (DEBUG) {
                Log.v(TAG, " transitionToState " + Debug.getLocation() + " current  = "
                        + Debug.getName(getContext(), mCurrentState)
                        + " to " + Debug.getName(getContext(), mEndState));
                Debug.logStack(TAG, " transitionToState  ", 4);
                Log.v(TAG, "-------------------------------------------");
            }
            setTransition(mCurrentState, id);

            animateTo(1.0f);

            mTransitionLastPosition = 0;
            transitionToEnd();
            if (duration > 0) {
                mTransitionDuration = duration / 1000f;
            }
            return;
        }
        if (DEBUG) {
            Log.v(TAG, "setTransition  unknown -> "
                    + Debug.getName(getContext(), id));
        }

        // TODO correctly use width & height
        mTemporalInterpolator = false;
        mTransitionGoalPosition = 1;
        mTransitionPosition = 0;
        mTransitionLastPosition = 0;
        mTransitionLastTime = getNanoTime();
        mAnimationStartTime = getNanoTime();
        mTransitionInstantly = false;
        mInterpolator = null;
        if (duration == -1) {
            mTransitionDuration = mScene.getDuration() / 1000f;
        }
        mBeginState = UNSET;
        mScene.setTransition(mBeginState, mEndState);
        SparseArray<MotionController> controllers = new SparseArray<>();
        if (duration == 0) {
            mTransitionDuration = mScene.getDuration() / 1000f;
        } else if (duration > 0) {
            mTransitionDuration = duration / 1000f;
        }

        int n = getChildCount();

        mFrameArrayList.clear();
        for (int i = 0; i < n; i++) {
            View v = getChildAt(i);
            MotionController f = new MotionController(v);
            mFrameArrayList.put(v, f);
            controllers.put(v.getId(), mFrameArrayList.get(v));
        }
        mInTransition = true;

        mModel.initFrom(mLayoutWidget, null, mScene.getConstraintSet(id));
        rebuildScene();
        mModel.build();
        computeCurrentPositions();
        int layoutWidth = getWidth();
        int layoutHeight = getHeight();
        // getMap the KeyFrames for each view

        if (mDecoratorsHelpers != null) {
            for (int i = 0; i < n; i++) {
                MotionController motionController = mFrameArrayList.get(getChildAt(i));
                if (motionController == null) {
                    continue;
                }
                mScene.getKeyFrames(motionController);
            }
            // Allow helpers to access all the motionControllers after
            for (MotionHelper mDecoratorsHelper : mDecoratorsHelpers) {
                mDecoratorsHelper.onPreSetup(this, mFrameArrayList);
            }
            for (int i = 0; i < n; i++) {
                MotionController motionController = mFrameArrayList.get(getChildAt(i));
                if (motionController == null) {
                    continue;
                }
                motionController.setup(layoutWidth, layoutHeight,
                        mTransitionDuration, getNanoTime());
            }
        } else {
            for (int i = 0; i < n; i++) {
                MotionController motionController = mFrameArrayList.get(getChildAt(i));
                if (motionController == null) {
                    continue;
                }
                mScene.getKeyFrames(motionController);
                motionController.setup(layoutWidth, layoutHeight,
                        mTransitionDuration, getNanoTime());
            }
        }

        float stagger = mScene.getStaggered();
        if (stagger != 0.0f) {
            float min = Float.MAX_VALUE, max = -Float.MAX_VALUE;
            for (int i = 0; i < n; i++) {
                MotionController f = mFrameArrayList.get(getChildAt(i));
                float x = f.getFinalX();
                float y = f.getFinalY();
                min = Math.min(min, y + x);
                max = Math.max(max, y + x);
            }

            for (int i = 0; i < n; i++) {
                MotionController f = mFrameArrayList.get(getChildAt(i));
                float x = f.getFinalX();
                float y = f.getFinalY();
                f.mStaggerScale = 1 / (1 - stagger);
                f.mStaggerOffset = stagger - stagger * (x + y - min) / (max - min);
            }
        }

        mTransitionPosition = 0;
        mTransitionLastPosition = 0;
        mInTransition = true;

        invalidate();
    }

    /**
     * Returns the last velocity used in the transition
     *
     * @return
     */
    public float getVelocity() {
        return mLastVelocity;
    }

    /**
     * Returns the last layout velocity used in the transition
     *
     * @param view           The view
     * @param posOnViewX     The x position on the view
     * @param posOnViewY     The y position on the view
     * @param returnVelocity The velocity
     * @param type           Velocity returned 0 = post layout, 1 = layout, 2 = static postlayout
     */
    public void getViewVelocity(View view,
                                float posOnViewX,
                                float posOnViewY,
                                float[] returnVelocity,
                                int type) {
        float v = mLastVelocity;
        float position = mTransitionLastPosition;
        if (mInterpolator != null) {
            float deltaT = EPSILON;
            float dir = Math.signum(mTransitionGoalPosition - mTransitionLastPosition);
            float interpolatedPosition =
                    mInterpolator.getInterpolation(mTransitionLastPosition + deltaT);
            position = mInterpolator.getInterpolation(mTransitionLastPosition);
            interpolatedPosition -= position;
            interpolatedPosition /= deltaT;
            v = dir * interpolatedPosition / mTransitionDuration;
        }

        if (mInterpolator instanceof MotionInterpolator) {
            v = ((MotionInterpolator) mInterpolator).getVelocity();

        }

        MotionController f = mFrameArrayList.get(view);
        if ((type & 1) == 0) {
            f.getPostLayoutDvDp(position,
                    view.getWidth(), view.getHeight(),
                    posOnViewX, posOnViewY, returnVelocity);
        } else {
            f.getDpDt(position, posOnViewX, posOnViewY, returnVelocity);
        }
        if (type < VELOCITY_STATIC_POST_LAYOUT) {
            returnVelocity[0] *= v;
            returnVelocity[1] *= v;
        }

    }

    ////////////////////////////////////////////////////////////////////////////////
    // This contains the logic for interacting with the ConstraintLayout Solver
    class Model {
        ConstraintWidgetContainer mLayoutStart = new ConstraintWidgetContainer();
        ConstraintWidgetContainer mLayoutEnd = new ConstraintWidgetContainer();
        ConstraintSet mStart = null;
        ConstraintSet mEnd = null;
        int mStartId;
        int mEndId;

        void copy(ConstraintWidgetContainer src, ConstraintWidgetContainer dest) {
            ArrayList<ConstraintWidget> children = src.getChildren();
            HashMap<ConstraintWidget, ConstraintWidget> map = new HashMap<>();
            map.put(src, dest);
            dest.getChildren().clear();
            dest.copy(src, map);
            for (ConstraintWidget child_s : children) {
                ConstraintWidget child_d;
                if (child_s instanceof androidx.constraintlayout.core.widgets.Barrier) {
                    child_d = new androidx.constraintlayout.core.widgets.Barrier();
                } else if (child_s instanceof androidx.constraintlayout.core.widgets.Guideline) {
                    child_d = new androidx.constraintlayout.core.widgets.Guideline();
                } else if (child_s instanceof Flow) {
                    child_d = new Flow();
                } else if (child_s instanceof Placeholder) {
                    child_d = new Placeholder();
                } else if (child_s instanceof androidx.constraintlayout.core.widgets.Helper) {
                    child_d = new androidx.constraintlayout.core.widgets.HelperWidget();
                } else {
                    child_d = new ConstraintWidget();
                }
                dest.add(child_d);
                map.put(child_s, child_d);
            }
            for (ConstraintWidget child_s : children) {
                map.get(child_s).copy(child_s, map);
            }
        }

        void initFrom(ConstraintWidgetContainer baseLayout,
                      ConstraintSet start,
                      ConstraintSet end) {
            mStart = start;
            mEnd = end;
            mLayoutStart =  new ConstraintWidgetContainer();
            mLayoutEnd =  new ConstraintWidgetContainer();
            mLayoutStart.setMeasurer(mLayoutWidget.getMeasurer());
            mLayoutEnd.setMeasurer(mLayoutWidget.getMeasurer());
            mLayoutStart.removeAllChildren();
            mLayoutEnd.removeAllChildren();
            copy(mLayoutWidget, mLayoutStart);
            copy(mLayoutWidget, mLayoutEnd);
            if (mTransitionLastPosition > 0.5) {
                if (start != null) {
                    setupConstraintWidget(mLayoutStart, start);
                }
                setupConstraintWidget(mLayoutEnd, end);
            } else {
                setupConstraintWidget(mLayoutEnd, end);
                if (start != null) {
                    setupConstraintWidget(mLayoutStart, start);
                }
            }
            // then init the engine...
            if (DEBUG) {
                Log.v(TAG, "> mLayoutStart.updateHierarchy " + Debug.getLocation());
            }
            mLayoutStart.setRtl(isRtl());
            mLayoutStart.updateHierarchy();

            if (DEBUG) {
                for (ConstraintWidget child : mLayoutStart.getChildren()) {
                    View view = (View) child.getCompanionWidget();
                    debugWidget(">>>>>>>  " + Debug.getName(view), child);
                }
                Log.v(TAG, "> mLayoutEnd.updateHierarchy " + Debug.getLocation());
                Log.v(TAG, "> mLayoutEnd.updateHierarchy  "
                        + Debug.getLocation() + "  == isRtl()=" + isRtl());
            }
            mLayoutEnd.setRtl(isRtl());
            mLayoutEnd.updateHierarchy();

            if (DEBUG) {
                for (ConstraintWidget child : mLayoutEnd.getChildren()) {
                    View view = (View) child.getCompanionWidget();
                    debugWidget(">>>>>>>  " + Debug.getName(view), child);
                }
            }
            ViewGroup.LayoutParams layoutParams = getLayoutParams();
            if (layoutParams != null) {
                if (layoutParams.width == WRAP_CONTENT) {
                    mLayoutStart.setHorizontalDimensionBehaviour(
                            ConstraintWidget.DimensionBehaviour.WRAP_CONTENT);
                    mLayoutEnd.setHorizontalDimensionBehaviour(
                            ConstraintWidget.DimensionBehaviour.WRAP_CONTENT);
                }
                if (layoutParams.height == WRAP_CONTENT) {
                    mLayoutStart.setVerticalDimensionBehaviour(
                            ConstraintWidget.DimensionBehaviour.WRAP_CONTENT);
                    mLayoutEnd.setVerticalDimensionBehaviour(
                            ConstraintWidget.DimensionBehaviour.WRAP_CONTENT);
                }
            }
        }

        private void setupConstraintWidget(ConstraintWidgetContainer base, ConstraintSet cSet) {
            SparseArray<ConstraintWidget> mapIdToWidget = new SparseArray<>();
            Constraints.LayoutParams layoutParams =
                    new Constraints.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                            ViewGroup.LayoutParams.WRAP_CONTENT);

            mapIdToWidget.clear();
            mapIdToWidget.put(PARENT_ID, base);
            mapIdToWidget.put(getId(), base);
            if (cSet != null && cSet.mRotate != 0) {
                resolveSystem(mLayoutEnd, getOptimizationLevel(),
                        MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY));
            }
            //  build id widget map
            for (ConstraintWidget child : base.getChildren()) {
                child.setAnimated(true);
                View view = (View) child.getCompanionWidget();
                mapIdToWidget.put(view.getId(), child);
            }

            for (ConstraintWidget child : base.getChildren()) {
                View view = (View) child.getCompanionWidget();
                cSet.applyToLayoutParams(view.getId(), layoutParams);

                child.setWidth(cSet.getWidth(view.getId()));
                child.setHeight(cSet.getHeight(view.getId()));
                if (view instanceof ConstraintHelper) {
                    cSet.applyToHelper((ConstraintHelper) view, child, layoutParams, mapIdToWidget);
                    if (view instanceof Barrier) {
                        ((Barrier) view).validateParams();
                        if (DEBUG) {
                            Log.v(TAG, ">>>>>>>>>> Barrier " + Debug.getName(getContext(),
                                    ((Barrier) view).getReferencedIds()));
                        }
                    }
                }
                if (DEBUG) {
                    debugLayoutParam(">>>>>>>  " + Debug.getName(view), layoutParams);
                }
                layoutParams.resolveLayoutDirection(getLayoutDirection());
                applyConstraintsFromLayoutParams(false, view, child, layoutParams, mapIdToWidget);
                if (cSet.getVisibilityMode(view.getId()) == ConstraintSet.VISIBILITY_MODE_IGNORE) {
                    child.setVisibility(view.getVisibility());
                } else {
                    child.setVisibility(cSet.getVisibility(view.getId()));
                }
            }
            for (ConstraintWidget child : base.getChildren()) {
                if (child instanceof androidx.constraintlayout.core.widgets.VirtualLayout) {
                    ConstraintHelper view = (ConstraintHelper) child.getCompanionWidget();
                    Helper helper = (Helper) child;
                    view.updatePreLayout(base, helper, mapIdToWidget);
                    androidx.constraintlayout.core.widgets.VirtualLayout virtualLayout =
                            (androidx.constraintlayout.core.widgets.VirtualLayout) helper;
                    virtualLayout.captureWidgets();
                }
            }
        }

        ConstraintWidget getWidget(ConstraintWidgetContainer container, View view) {
            if (container.getCompanionWidget() == view) {
                return container;
            }
            ArrayList<ConstraintWidget> children = container.getChildren();
            final int count = children.size();
            for (int i = 0; i < count; i++) {
                ConstraintWidget widget = children.get(i);
                if (widget.getCompanionWidget() == view) {
                    return widget;
                }

            }
            return null;
        }

        @SuppressLint("LogConditional")
        private void debugLayoutParam(String str, LayoutParams params) {
            String a = " ";
            a += params.startToStart != UNSET ? "SS" : "__";
            a += params.startToEnd != UNSET ? "|SE" : "|__";
            a += params.endToStart != UNSET ? "|ES" : "|__";
            a += params.endToEnd != UNSET ? "|EE" : "|__";
            a += params.leftToLeft != UNSET ? "|LL" : "|__";
            a += params.leftToRight != UNSET ? "|LR" : "|__";
            a += params.rightToLeft != UNSET ? "|RL" : "|__";
            a += params.rightToRight != UNSET ? "|RR" : "|__";
            a += params.topToTop != UNSET ? "|TT" : "|__";
            a += params.topToBottom != UNSET ? "|TB" : "|__";
            a += params.bottomToTop != UNSET ? "|BT" : "|__";
            a += params.bottomToBottom != UNSET ? "|BB" : "|__";
            Log.v(TAG, str + a);
        }

        @SuppressLint("LogConditional")
        private void debugWidget(String str, ConstraintWidget child) {
            String a = " ";
            a += child.mTop.mTarget != null
                    ? ("T" + (child.mTop.mTarget.mType == ConstraintAnchor.Type.TOP ? "T" : "B"))
                    : "__";
            a += child.mBottom.mTarget != null
                    ? ("B" + (child.mBottom.mTarget.mType == ConstraintAnchor.Type.TOP ? "T" : "B"))
                    : "__";
            a += child.mLeft.mTarget != null
                    ? ("L" + (child.mLeft.mTarget.mType == ConstraintAnchor.Type.LEFT ? "L" : "R"))
                    : "__";
            a += child.mRight.mTarget != null
                    ? ("R" + (child.mRight.mTarget.mType == ConstraintAnchor.Type.LEFT ? "L" : "R"))
                    : "__";
            Log.v(TAG, str + a + " ---  " + child);
        }

        @SuppressLint("LogConditional")
        private void debugLayout(String title, ConstraintWidgetContainer c) {
            View v = (View) c.getCompanionWidget();
            String cName = title + " " + Debug.getName(v);
            Log.v(TAG, cName + "  ========= " + c);
            int count = c.getChildren().size();
            for (int i = 0; i < count; i++) {
                String str = cName + "[" + i + "] ";
                ConstraintWidget child = c.getChildren().get(i);
                String a = "";
                a += child.mTop.mTarget != null ? "T" : "_";
                a += child.mBottom.mTarget != null ? "B" : "_";
                a += child.mLeft.mTarget != null ? "L" : "_";
                a += child.mRight.mTarget != null ? "R" : "_";
                v = (View) child.getCompanionWidget();
                String name = Debug.getName(v);
                if (v instanceof TextView) {
                    name += "(" + ((TextView) v).getText() + ")";
                }
                Log.v(TAG, str + "  " + name + " " + child + " " + a);
            }
            Log.v(TAG, cName + " done. ");
        }

        public void reEvaluateState() {
            measure(mLastWidthMeasureSpec, mLastHeightMeasureSpec);
            setupMotionViews();
        }

        public void measure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);

            mWidthMeasureMode = widthMode;
            mHeightMeasureMode = heightMode;

            computeStartEndSize(widthMeasureSpec, heightMeasureSpec);

            // This works around the problem that MotionLayout calls its children
            // Wrap content children
            // with measure(AT_MOST,AT_MOST) then measure(EXACTLY, EXACTLY)
            // if a child of MotionLayout is a motionLayout
            // it would not know it could resize during animation
            // other Layouts may have this behaviour but for now this is the only one we support

            boolean recompute_start_end_size = true;
            if (getParent() instanceof MotionLayout
                    && widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY) {
                recompute_start_end_size = false;
            }
            if (recompute_start_end_size) {
                computeStartEndSize(widthMeasureSpec, heightMeasureSpec);

                mStartWrapWidth = mLayoutStart.getWidth();
                mStartWrapHeight = mLayoutStart.getHeight();
                mEndWrapWidth = mLayoutEnd.getWidth();
                mEndWrapHeight = mLayoutEnd.getHeight();
                mMeasureDuringTransition = ((mStartWrapWidth != mEndWrapWidth)
                        || (mStartWrapHeight != mEndWrapHeight));
            }

            int width = mStartWrapWidth;
            int height = mStartWrapHeight;
            if (mWidthMeasureMode == MeasureSpec.AT_MOST
                    || mWidthMeasureMode == MeasureSpec.UNSPECIFIED) {
                width = (int) (mStartWrapWidth + mPostInterpolationPosition
                        * (mEndWrapWidth - mStartWrapWidth));
            }
            if (mHeightMeasureMode == MeasureSpec.AT_MOST
                    || mHeightMeasureMode == MeasureSpec.UNSPECIFIED) {
                height = (int) (mStartWrapHeight + mPostInterpolationPosition
                        * (mEndWrapHeight - mStartWrapHeight));
            }

            boolean isWidthMeasuredTooSmall = mLayoutStart.isWidthMeasuredTooSmall()
                    || mLayoutEnd.isWidthMeasuredTooSmall();
            boolean isHeightMeasuredTooSmall = mLayoutStart.isHeightMeasuredTooSmall()
                    || mLayoutEnd.isHeightMeasuredTooSmall();
            resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec,
                    width, height, isWidthMeasuredTooSmall, isHeightMeasuredTooSmall);

            if (DEBUG) {
                Debug.logStack(TAG, ">>>>>>>>", 3);
                debugLayout(">>>>>>> measure str ", mLayoutStart);
                debugLayout(">>>>>>> measure end ", mLayoutEnd);
            }
        }

        private void computeStartEndSize(int widthMeasureSpec, int heightMeasureSpec) {
            int optimisationLevel = getOptimizationLevel();

            if (mCurrentState == getStartState()) {
                resolveSystem(mLayoutEnd, optimisationLevel,
                        (mEnd == null || mEnd.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec,
                        (mEnd == null || mEnd.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec);
                if (mStart != null) {
                    resolveSystem(mLayoutStart, optimisationLevel,
                            (mStart.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec,
                            (mStart.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec);
                }
            } else {
                if (mStart != null) {
                    resolveSystem(mLayoutStart, optimisationLevel,
                            (mStart.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec,
                            (mStart.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec);
                }
                resolveSystem(mLayoutEnd, optimisationLevel,
                        (mEnd == null || mEnd.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec,
                        (mEnd == null || mEnd.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec);
            }
        }

        public void build() {
            final int n = getChildCount();
            mFrameArrayList.clear();
            SparseArray<MotionController> controllers = new SparseArray<>();
            int[] ids = new int[n];
            for (int i = 0; i < n; i++) {
                View v = getChildAt(i);
                MotionController motionController = new MotionController(v);
                controllers.put(ids[i] = v.getId(), motionController);
                mFrameArrayList.put(v, motionController);
            }
            for (int i = 0; i < n; i++) {
                View v = getChildAt(i);
                MotionController motionController = mFrameArrayList.get(v);
                if (motionController == null) {
                    continue;
                }
                if (mStart != null) {
                    ConstraintWidget startWidget = getWidget(mLayoutStart, v);
                    if (startWidget != null) {
                        motionController.setStartState(toRect(startWidget), mStart,
                                getWidth(), getHeight());
                    } else {
                        if (mDebugPath != 0) {
                            Log.e(TAG, Debug.getLocation() + "no widget for  "
                                    + Debug.getName(v) + " (" + v.getClass().getName() + ")");
                        }
                    }
                } else {
                    if (mInRotation) {
                        motionController.setStartState(mPreRotate.get(v), v, mRotatMode,
                                mPreRotateWidth, mPreRotateHeight);
                    }
                }
                if (mEnd != null) {
                    ConstraintWidget endWidget = getWidget(mLayoutEnd, v);
                    if (endWidget != null) {
                        motionController.setEndState(toRect(endWidget), mEnd,
                                getWidth(), getHeight());
                    } else {
                        if (mDebugPath != 0) {
                            Log.e(TAG, Debug.getLocation() + "no widget for  "
                                    + Debug.getName(v)
                                    + " (" + v.getClass().getName() + ")");
                        }
                    }
                }
            }

            for (int i = 0; i < n; i++) {
                MotionController controller = controllers.get(ids[i]);
                int relativeToId = controller.getAnimateRelativeTo();
                if (relativeToId != UNSET) {
                    controller.setupRelative(controllers.get(relativeToId));
                }
            }
        }

        public void setMeasuredId(int startId, int endId) {
            mStartId = startId;
            mEndId = endId;
        }

        public boolean isNotConfiguredWith(int startId, int endId) {
            return startId != mStartId || endId != mEndId;
        }
    }

    Model mModel = new Model();

    private Rect toRect(ConstraintWidget cw) {
        mTempRect.top = cw.getY();
        mTempRect.left = cw.getX();
        mTempRect.right = cw.getWidth() + mTempRect.left;
        mTempRect.bottom = cw.getHeight() + mTempRect.top;
        return mTempRect;
    }

    @Override
    public void requestLayout() {
        if (!mMeasureDuringTransition) {
            if (mCurrentState == UNSET && mScene != null
                    && mScene.mCurrentTransition != null) {
                int mode = mScene.mCurrentTransition.getLayoutDuringTransition();
                if (mode == MotionScene.LAYOUT_IGNORE_REQUEST) {
                    return;
                } else if (mode == MotionScene.LAYOUT_CALL_MEASURE) {
                    final int n = getChildCount();
                    for (int i = 0; i < n; i++) {
                        View v = getChildAt(i);
                        MotionController motionController = mFrameArrayList.get(v);
                        motionController.remeasure();
                    }
                    return;
                }
            }
        }
        super.requestLayout();
    }

    @Override
    public String toString() {
        Context context = getContext();
        return Debug.getName(context, mBeginState) + "->"
                + Debug.getName(context, mEndState)
                + " (pos:" + mTransitionLastPosition + " Dpos/Dt:" + mLastVelocity;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (DEBUG) {
            Log.v(TAG, "onMeasure " + Debug.getLocation());
        }
        if (mScene == null) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }
        boolean recalc = (mLastWidthMeasureSpec != widthMeasureSpec
                || mLastHeightMeasureSpec != heightMeasureSpec);
        if (mNeedsFireTransitionCompleted) {
            mNeedsFireTransitionCompleted = false;
            onNewStateAttachHandlers();
            processTransitionCompleted();
            recalc = true;
        }

        if (mDirtyHierarchy) {
            recalc = true;
        }

        mLastWidthMeasureSpec = widthMeasureSpec;
        mLastHeightMeasureSpec = heightMeasureSpec;

        int startId = mScene.getStartId();
        int endId = mScene.getEndId();
        boolean setMeasure = true;
        if ((recalc || mModel.isNotConfiguredWith(startId, endId)) && mBeginState != UNSET) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            mModel.initFrom(mLayoutWidget, mScene.getConstraintSet(startId),
                    mScene.getConstraintSet(endId));
            mModel.reEvaluateState();
            mModel.setMeasuredId(startId, endId);
            setMeasure = false;
        } else if (recalc) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

        if (mMeasureDuringTransition || setMeasure) {
            int heightPadding = getPaddingTop() + getPaddingBottom();
            int widthPadding = getPaddingLeft() + getPaddingRight();
            int androidLayoutWidth = mLayoutWidget.getWidth() + widthPadding;
            int androidLayoutHeight = mLayoutWidget.getHeight() + heightPadding;
            if (mWidthMeasureMode == MeasureSpec.AT_MOST
                    || mWidthMeasureMode == MeasureSpec.UNSPECIFIED) {
                androidLayoutWidth = (int) (mStartWrapWidth + mPostInterpolationPosition
                        * (mEndWrapWidth - mStartWrapWidth));
                requestLayout();
            }
            if (mHeightMeasureMode == MeasureSpec.AT_MOST
                    || mHeightMeasureMode == MeasureSpec.UNSPECIFIED) {
                androidLayoutHeight = (int) (mStartWrapHeight + mPostInterpolationPosition
                        * (mEndWrapHeight - mStartWrapHeight));
                requestLayout();
            }
            setMeasuredDimension(androidLayoutWidth, androidLayoutHeight);
        }
        evaluateLayout();
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child,
                                       @NonNull View target,
                                       int axes, int type) {
        if (DEBUG) {
            Log.v(TAG, "********** onStartNestedScroll( child:" + Debug.getName(child)
                    + ", target:" + Debug.getName(target) + ", axis:" + axes + ", type:" + type);
        }
        if (mScene == null
                || mScene.mCurrentTransition == null
                || mScene.mCurrentTransition.getTouchResponse() == null
                || (mScene.mCurrentTransition.getTouchResponse().getFlags()
                & TouchResponse.FLAG_DISABLE_SCROLL) != 0) {
            return false;
        }
        return true;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child,
                                       @NonNull View target,
                                       int axes,
                                       int type) {
        if (DEBUG) {
            Log.v(TAG, "********** onNestedScrollAccepted( child:" + Debug.getName(child)
                    + ", target:" + Debug.getName(target) + ", axis:" + axes + ", type:" + type);
        }
        mScrollTargetTime = getNanoTime();
        mScrollTargetDT = 0;
        mScrollTargetDX = 0;
        mScrollTargetDY = 0;
    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        if (DEBUG) {
            Log.v(TAG, "********** onStopNestedScroll(   target:"
                    + Debug.getName(target) + " , type:" + type + " "
                    + mScrollTargetDX + ", " + mScrollTargetDY);
            Debug.logStack(TAG, "onStopNestedScroll ", 8);

        }
        if (mScene == null || mScrollTargetDT == 0) {
            return;
        }
        mScene.processScrollUp(mScrollTargetDX / mScrollTargetDT,
                mScrollTargetDY / mScrollTargetDT);
    }

    @Override
    public void onNestedScroll(@NonNull View target,
                               int dxConsumed,
                               int dyConsumed,
                               int dxUnconsumed,
                               int dyUnconsumed,
                               int type, int[] consumed) {
        if (mUndergoingMotion || dxConsumed != 0 || dyConsumed != 0) {
            consumed[0] += dxUnconsumed;
            consumed[1] += dyUnconsumed;
        }
        mUndergoingMotion = false;
    }

    @Override
    public void onNestedScroll(@NonNull View target,
                               int dxConsumed,
                               int dyConsumed,
                               int dxUnconsumed,
                               int dyUnconsumed,
                               int type) {
        if (DEBUG) {
            Log.v(TAG, "********** onNestedScroll( target:" + Debug.getName(target)
                    + ", dxConsumed:" + dxConsumed
                    + ", dyConsumed:" + dyConsumed
                    + ", dyConsumed:" + dxUnconsumed
                    + ", dyConsumed:" + dyUnconsumed + ", type:" + type);
        }
    }

    @Override
    public void onNestedPreScroll(@NonNull View target,
                                  int dx,
                                  int dy,
                                  @NonNull int[] consumed,
                                  int type) {

        MotionScene scene = mScene;
        if (scene == null) {
            return;
        }

        MotionScene.Transition currentTransition = scene.mCurrentTransition;
        if (currentTransition == null || !currentTransition.isEnabled()) {
            return;
        }

        if (currentTransition.isEnabled()) {
            TouchResponse touchResponse = currentTransition.getTouchResponse();
            if (touchResponse != null) {
                int regionId = touchResponse.getTouchRegionId();
                if (regionId != MotionScene.UNSET && target.getId() != regionId) {
                    return;
                }
            }
        }

        if (scene.getMoveWhenScrollAtTop()) {
            // This blocks transition during scrolling
            TouchResponse touchResponse = currentTransition.getTouchResponse();
            int vert = -1;
            if (touchResponse != null) {
                if ((touchResponse.getFlags() & TouchResponse.FLAG_SUPPORT_SCROLL_UP) != 0) {
                    vert = dy;
                }
            }
            if ((mTransitionPosition == 1 || mTransitionPosition == 0)
                    && target.canScrollVertically(vert)) {
                return;
            }
        }

        // This should be disabled in androidx
        if (currentTransition.getTouchResponse() != null
                && (currentTransition.getTouchResponse().getFlags()
                & TouchResponse.FLAG_DISABLE_POST_SCROLL) != 0) {
            float dir = scene.getProgressDirection(dx, dy);
            if ((mTransitionLastPosition <= 0.0f && (dir < 0))
                    || (mTransitionLastPosition >= 1.0f && (dir > 0))) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    target.setNestedScrollingEnabled(false);
                    // TODO find a better hack
                    target.post(new Runnable() {
                        @Override
                        public void run() {
                            target.setNestedScrollingEnabled(true);
                        }
                    });
                }
                return;
            }
        }

        if (DEBUG) {
            Log.v(TAG, "********** onNestedPreScroll(target:"
                    + Debug.getName(target) + ", dx:" + dx + ", dy:" + dy + ", type:" + type);
        }
        float progress = mTransitionPosition;
        long time = getNanoTime();
        mScrollTargetDX = dx;
        mScrollTargetDY = dy;
        mScrollTargetDT = (float) ((time - mScrollTargetTime) * 1E-9);
        mScrollTargetTime = time;
        if (DEBUG) {
            Log.v(TAG, "********** dy = " + dx + " dy = " + dy + " dt = " + mScrollTargetDT);
        }
        scene.processScrollMove(dx, dy);
        if (progress != mTransitionPosition) {
            consumed[0] = dx;
            consumed[1] = dy;
        }
        evaluate(false);
        if (consumed[0] != 0 || consumed[1] != 0) {
            mUndergoingMotion = true;
        }

    }

    @Override
    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public boolean onNestedFling(@NonNull View target,
                                 float velocityX,
                                 float velocityY,
                                 boolean consumed) {
        return false;
    }

    ////////////////////////////////////////////////////////////////////////////////////////
    // Used to draw debugging lines
    ////////////////////////////////////////////////////////////////////////////////////////
    private class DevModeDraw {
        private static final int DEBUG_PATH_TICKS_PER_MS = 16;
        float[] mPoints;
        int[] mPathMode;
        float[] mKeyFramePoints;
        Path mPath;
        Paint mPaint;
        Paint mPaintKeyframes;
        Paint mPaintGraph;
        Paint mTextPaint;
        Paint mFillPaint;
        private float[] mRectangle;
        final int mRedColor = 0xFFFFAA33;
        final int mKeyframeColor = 0xffe0759a;
        final int mGraphColor = 0xFF33AA00;
        final int mShadowColor = 0x77000000;
        final int mDiamondSize = 10;
        DashPathEffect mDashPathEffect;
        int mKeyFrameCount;
        Rect mBounds = new Rect();
        boolean mPresentationMode = false;
        int mShadowTranslate = 1;

        DevModeDraw() {
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setColor(mRedColor);
            mPaint.setStrokeWidth(2);
            mPaint.setStyle(Paint.Style.STROKE);

            mPaintKeyframes = new Paint();
            mPaintKeyframes.setAntiAlias(true);
            mPaintKeyframes.setColor(mKeyframeColor);
            mPaintKeyframes.setStrokeWidth(2);
            mPaintKeyframes.setStyle(Paint.Style.STROKE);

            mPaintGraph = new Paint();
            mPaintGraph.setAntiAlias(true);
            mPaintGraph.setColor(mGraphColor);
            mPaintGraph.setStrokeWidth(2);
            mPaintGraph.setStyle(Paint.Style.STROKE);

            mTextPaint = new Paint();
            mTextPaint.setAntiAlias(true);
            mTextPaint.setColor(mGraphColor);
            mTextPaint.setTextSize(12 * getContext().getResources().getDisplayMetrics().density);
            mRectangle = new float[8];
            mFillPaint = new Paint();
            mFillPaint.setAntiAlias(true);
            mDashPathEffect = new DashPathEffect(new float[]{4, 8}, 0);
            mPaintGraph.setPathEffect(mDashPathEffect);
            mKeyFramePoints = new float[MAX_KEY_FRAMES * 2];
            mPathMode = new int[MAX_KEY_FRAMES];

            if (mPresentationMode) {
                mPaint.setStrokeWidth(8);
                mFillPaint.setStrokeWidth(8);
                mPaintKeyframes.setStrokeWidth(8);
                mShadowTranslate = 4;
            }
        }

        public void draw(Canvas canvas,
                         HashMap<View, MotionController> frameArrayList,
                         int duration, int debugPath) {
            if (frameArrayList == null || frameArrayList.size() == 0) {
                return;
            }
            canvas.save();
            if (!isInEditMode() && (DEBUG_SHOW_PROGRESS & debugPath) == DEBUG_SHOW_PATH) {
                String str = getContext().getResources().getResourceName(mEndState)
                        + ":" + getProgress();
                canvas.drawText(str, 10, getHeight() - 30, mTextPaint);
                canvas.drawText(str, 11, getHeight() - 29, mPaint);
            }
            for (MotionController motionController : frameArrayList.values()) {
                int mode = motionController.getDrawPath();
                if (debugPath > 0 && mode == MotionController.DRAW_PATH_NONE) {
                    mode = MotionController.DRAW_PATH_BASIC;
                }
                if (mode == MotionController.DRAW_PATH_NONE) { // do not draw path
                    continue;
                }

                mKeyFrameCount = motionController.buildKeyFrames(mKeyFramePoints, mPathMode);

                if (mode >= MotionController.DRAW_PATH_BASIC) {

                    int frames = duration / DEBUG_PATH_TICKS_PER_MS;
                    if (mPoints == null || mPoints.length != frames * 2) {
                        mPoints = new float[frames * 2];
                        mPath = new Path();
                    }

                    canvas.translate(mShadowTranslate, mShadowTranslate);

                    mPaint.setColor(mShadowColor);
                    mFillPaint.setColor(mShadowColor);
                    mPaintKeyframes.setColor(mShadowColor);
                    mPaintGraph.setColor(mShadowColor);
                    motionController.buildPath(mPoints, frames);
                    drawAll(canvas, mode, mKeyFrameCount, motionController);
                    mPaint.setColor(mRedColor);
                    mPaintKeyframes.setColor(mKeyframeColor);
                    mFillPaint.setColor(mKeyframeColor);
                    mPaintGraph.setColor(mGraphColor);

                    canvas.translate(-mShadowTranslate, -mShadowTranslate);
                    drawAll(canvas, mode, mKeyFrameCount, motionController);
                    if (mode == MotionController.DRAW_PATH_RECTANGLE) {
                        drawRectangle(canvas, motionController);
                    }
                }

            }
            canvas.restore();
        }

        public void drawAll(Canvas canvas,
                            int mode,
                            int keyFrames,
                            MotionController motionController) {
            if (mode == MotionController.DRAW_PATH_AS_CONFIGURED) {
                drawPathAsConfigured(canvas);
            }
            if (mode == MotionController.DRAW_PATH_RELATIVE) {
                drawPathRelative(canvas);
            }
            if (mode == MotionController.DRAW_PATH_CARTESIAN) {
                drawPathCartesian(canvas);
            }
            drawBasicPath(canvas);
            drawTicks(canvas, mode, keyFrames, motionController);
        }

        private void drawBasicPath(Canvas canvas) {
            canvas.drawLines(mPoints, mPaint);
        }

        private void drawTicks(Canvas canvas,
                               int mode,
                               int keyFrames,
                               MotionController motionController) {
            int viewWidth = 0;
            int viewHeight = 0;
            if (motionController.mView != null) {
                viewWidth = motionController.mView.getWidth();
                viewHeight = motionController.mView.getHeight();
            }
            for (int i = 1; i < keyFrames - 1; i++) {
                if (mode == MotionController.DRAW_PATH_AS_CONFIGURED
                        && mPathMode[i - 1] == MotionController.DRAW_PATH_NONE) {
                    continue;

                }
                float x = mKeyFramePoints[i * 2];
                float y = mKeyFramePoints[i * 2 + 1];
                mPath.reset();
                mPath.moveTo(x, y + mDiamondSize);
                mPath.lineTo(x + mDiamondSize, y);
                mPath.lineTo(x, y - mDiamondSize);
                mPath.lineTo(x - mDiamondSize, y);
                mPath.close();

                @SuppressWarnings("unused")
                MotionPaths framePoint = motionController.getKeyFrame(i - 1);
                float dx = 0; //framePoint.translationX;
                float dy = 0; //framePoint.translationY;
                if (mode == MotionController.DRAW_PATH_AS_CONFIGURED) {

                    if (mPathMode[i - 1] == MotionPaths.PERPENDICULAR) {
                        drawPathRelativeTicks(canvas, x - dx, y - dy);
                    } else if (mPathMode[i - 1] == MotionPaths.CARTESIAN) {
                        drawPathCartesianTicks(canvas, x - dx, y - dy);
                    } else if (mPathMode[i - 1] == MotionPaths.SCREEN) {
                        drawPathScreenTicks(canvas, x - dx, y - dy, viewWidth, viewHeight);
                    }

                    canvas.drawPath(mPath, mFillPaint);
                }
                if (mode == MotionController.DRAW_PATH_RELATIVE) {
                    drawPathRelativeTicks(canvas, x - dx, y - dy);
                }
                if (mode == MotionController.DRAW_PATH_CARTESIAN) {
                    drawPathCartesianTicks(canvas, x - dx, y - dy);
                }
                if (mode == MotionController.DRAW_PATH_SCREEN) {
                    drawPathScreenTicks(canvas, x - dx, y - dy, viewWidth, viewHeight);
                }
                if (dx != 0 || dy != 0) {
                    drawTranslation(canvas, x - dx, y - dy, x, y);
                } else {
                    canvas.drawPath(mPath, mFillPaint);
                }
            }
            if (mPoints.length > 1) {
                // Draw the starting and ending circle
                canvas.drawCircle(mPoints[0], mPoints[1], 8, mPaintKeyframes);
                canvas.drawCircle(mPoints[mPoints.length - 2],
                        mPoints[mPoints.length - 1], 8, mPaintKeyframes);
            }
        }

        private void drawTranslation(Canvas canvas, float x1, float y1, float x2, float y2) {
            canvas.drawRect(x1, y1, x2, y2, mPaintGraph);
            canvas.drawLine(x1, y1, x2, y2, mPaintGraph);
        }

        private void drawPathRelative(Canvas canvas) {
            canvas.drawLine(mPoints[0], mPoints[1],
                    mPoints[mPoints.length - 2], mPoints[mPoints.length - 1], mPaintGraph);
        }

        private void drawPathAsConfigured(Canvas canvas) {
            boolean path = false;
            boolean cart = false;
            for (int i = 0; i < mKeyFrameCount; i++) {
                if (mPathMode[i] == MotionPaths.PERPENDICULAR) {
                    path = true;
                }
                if (mPathMode[i] == MotionPaths.CARTESIAN) {
                    cart = true;
                }
            }
            if (path) {
                drawPathRelative(canvas);
            }
            if (cart) {
                drawPathCartesian(canvas);
            }
        }

        private void drawPathRelativeTicks(Canvas canvas, float x, float y) {
            float x1 = mPoints[0];
            float y1 = mPoints[1];
            float x2 = mPoints[mPoints.length - 2];
            float y2 = mPoints[mPoints.length - 1];
            float dist = (float) Math.hypot(x1 - x2, y1 - y2);
            float t = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / (dist * dist);
            float xp = x1 + t * (x2 - x1);
            float yp = y1 + t * (y2 - y1);

            Path path = new Path();
            path.moveTo(x, y);
            path.lineTo(xp, yp);
            float len = (float) Math.hypot(xp - x, yp - y);
            String text = "" + ((int) (100 * len / dist)) / 100.0f;
            getTextBounds(text, mTextPaint);
            float off = len / 2 - mBounds.width() / 2;
            canvas.drawTextOnPath(text, path, off, -20, mTextPaint);
            canvas.drawLine(x, y, xp, yp, mPaintGraph);
        }

        void getTextBounds(String text, Paint paint) {
            paint.getTextBounds(text, 0, text.length(), mBounds);
        }

        private void drawPathCartesian(Canvas canvas) {
            float x1 = mPoints[0];
            float y1 = mPoints[1];
            float x2 = mPoints[mPoints.length - 2];
            float y2 = mPoints[mPoints.length - 1];

            canvas.drawLine(Math.min(x1, x2), Math.max(y1, y2),
                    Math.max(x1, x2), Math.max(y1, y2), mPaintGraph);
            canvas.drawLine(Math.min(x1, x2), Math.min(y1, y2),
                    Math.min(x1, x2), Math.max(y1, y2), mPaintGraph);
        }

        private void drawPathCartesianTicks(Canvas canvas, float x, float y) {
            float x1 = mPoints[0];
            float y1 = mPoints[1];
            float x2 = mPoints[mPoints.length - 2];
            float y2 = mPoints[mPoints.length - 1];
            float minx = Math.min(x1, x2);
            float maxy = Math.max(y1, y2);
            float xgap = x - Math.min(x1, x2);
            float ygap = Math.max(y1, y2) - y;
            // Horizontal line
            String text = "" + ((int) (0.5 + 100 * xgap / Math.abs(x2 - x1))) / 100.0f;
            getTextBounds(text, mTextPaint);
            float off = xgap / 2 - mBounds.width() / 2;
            canvas.drawText(text, off + minx, y - 20, mTextPaint);
            canvas.drawLine(x, y,
                    Math.min(x1, x2), y, mPaintGraph);

            // Vertical line
            text = "" + ((int) (0.5 + 100 * ygap / Math.abs(y2 - y1))) / 100.0f;
            getTextBounds(text, mTextPaint);
            off = ygap / 2 - mBounds.height() / 2;
            canvas.drawText(text, x + 5, maxy - off, mTextPaint);
            canvas.drawLine(x, y,
                    x, Math.max(y1, y2), mPaintGraph);
        }

        private void drawPathScreenTicks(Canvas canvas,
                                         float x,
                                         float y,
                                         int viewWidth,
                                         int viewHeight) {
            float x1 = 0;
            float y1 = 0;
            float x2 = 1;
            float y2 = 1;
            float minx = 0;
            float maxy = 0;
            float xgap = x;
            float ygap = y;
            // Horizontal line
            String text = "" + ((int) (0.5 + 100 * (xgap - viewWidth / 2)
                    / (getWidth() - viewWidth))) / 100.0f;
            getTextBounds(text, mTextPaint);
            float off = xgap / 2 - mBounds.width() / 2;
            canvas.drawText(text, off + minx, y - 20, mTextPaint);
            canvas.drawLine(x, y,
                    Math.min(x1, x2), y, mPaintGraph);

            // Vertical line
            text = "" + ((int) (0.5 + 100 * (ygap - viewHeight / 2)
                    / (getHeight() - viewHeight))) / 100.0f;
            getTextBounds(text, mTextPaint);
            off = ygap / 2 - mBounds.height() / 2;
            canvas.drawText(text, x + 5, maxy - off, mTextPaint);
            canvas.drawLine(x, y,
                    x, Math.max(y1, y2), mPaintGraph);
        }

        private void drawRectangle(Canvas canvas, MotionController motionController) {
            mPath.reset();
            int rectFrames = 50;
            for (int i = 0; i <= rectFrames; i++) {
                float p = i / (float) rectFrames;
                motionController.buildRect(p, mRectangle, 0);
                mPath.moveTo(mRectangle[0], mRectangle[1]);
                mPath.lineTo(mRectangle[2], mRectangle[3]);
                mPath.lineTo(mRectangle[4], mRectangle[5]);
                mPath.lineTo(mRectangle[6], mRectangle[7]);
                mPath.close();
            }
            mPaint.setColor(0x44000000);
            canvas.translate(2, 2);
            canvas.drawPath(mPath, mPaint);

            canvas.translate(-2, -2);
            mPaint.setColor(0xFFFF0000);
            canvas.drawPath(mPath, mPaint);
        }

    }

    @SuppressLint("LogConditional")
    private void debugPos() {
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            Log.v(TAG, " " + Debug.getLocation() + " " + Debug.getName(this)
                    + " " + Debug.getName(getContext(), mCurrentState) + " " + Debug.getName(child)
                    + child.getLeft() + " "
                    + child.getTop());
        }
    }

    /**
     * Used to draw debugging graphics and to do post layout changes
     *
     * @param canvas
     */
    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (DEBUG) {
            Log.v(TAG, " dispatchDraw " + getProgress() + Debug.getLocation());
        }
        if (mDecoratorsHelpers != null) {
            for (MotionHelper decor : mDecoratorsHelpers) {
                decor.onPreDraw(canvas);
            }
        }
        evaluate(false);
        if (mScene != null && mScene.mViewTransitionController != null) {
            mScene.mViewTransitionController.animate();
        }
        if (DEBUG) {
            Log.v(TAG, " dispatchDraw" + Debug.getLocation() + " " + Debug.getName(this)
                    + " " + Debug.getName(getContext(), mCurrentState));
            debugPos();
        }
        super.dispatchDraw(canvas);
        if (mScene == null) {
            return;
        }
        if (DEBUG) {
            mDebugPath = 0xFF;
        }
        if ((mDebugPath & 1) == 1) {
            if (!isInEditMode()) {
                mFrames++;
                long currentDrawTime = getNanoTime();
                if (mLastDrawTime != -1) {
                    long delay = currentDrawTime - mLastDrawTime;
                    if (delay > 200000000) {
                        float fps = mFrames / (delay * 1E-9f);
                        mLastFps = ((int) (fps * 100)) / 100.0f;
                        mFrames = 0;
                        mLastDrawTime = currentDrawTime;
                    }
                } else {
                    mLastDrawTime = currentDrawTime;
                }
                Paint paint = new Paint();
                paint.setTextSize(42);
                float p = ((int) (getProgress() * 1000)) / 10f;
                String str = mLastFps + " fps " + Debug.getState(this, mBeginState) + " -> ";
                str += Debug.getState(this, mEndState) + " (progress: " + p + " ) state="
                        + ((mCurrentState == UNSET) ? "undefined"
                                : Debug.getState(this, mCurrentState));
                paint.setColor(0xFF000000);
                canvas.drawText(str, 11, getHeight() - 29, paint);
                paint.setColor(0xFF880088);
                canvas.drawText(str, 10, getHeight() - 30, paint);

            }
        }
        if (mDebugPath > 1) {
            if (mDevModeDraw == null) {
                mDevModeDraw = new DevModeDraw();
            }
            mDevModeDraw.draw(canvas, mFrameArrayList, mScene.getDuration(), mDebugPath);
        }
        if (mDecoratorsHelpers != null) {
            for (MotionHelper decor : mDecoratorsHelpers) {
                decor.onPostDraw(canvas);
            }
        }
    }

    /**
     * Direct layout evaluation
     */
    private void evaluateLayout() {
        float dir = Math.signum(mTransitionGoalPosition - mTransitionLastPosition);
        long currentTime = getNanoTime();

        float deltaPos = 0f;
        if (!(mInterpolator instanceof StopLogic)) { // if we are not in a drag
            deltaPos = dir * (currentTime - mTransitionLastTime) * 1E-9f / mTransitionDuration;
        }
        float position = mTransitionLastPosition + deltaPos;

        boolean done = false;
        if (mTransitionInstantly) {
            position = mTransitionGoalPosition;
        }

        if ((dir > 0 && position >= mTransitionGoalPosition)
                || (dir <= 0 && position <= mTransitionGoalPosition)) {
            position = mTransitionGoalPosition;
            done = true;
        }
        if (mInterpolator != null && !done) {
            if (mTemporalInterpolator) {
                float time = (currentTime - mAnimationStartTime) * 1E-9f;
                position = mInterpolator.getInterpolation(time);
            } else {
                position = mInterpolator.getInterpolation(position);
            }
        }
        if ((dir > 0 && position >= mTransitionGoalPosition)
                || (dir <= 0 && position <= mTransitionGoalPosition)) {
            position = mTransitionGoalPosition;
        }
        mPostInterpolationPosition = position;
        int n = getChildCount();
        long time = getNanoTime();
        float interPos = mProgressInterpolator == null ? position
                : mProgressInterpolator.getInterpolation(position);
        for (int i = 0; i < n; i++) {
            final View child = getChildAt(i);
            final MotionController frame = mFrameArrayList.get(child);
            if (frame != null) {
                frame.interpolate(child, interPos, time, mKeyCache);
            }
        }
        if (mMeasureDuringTransition) {
            requestLayout();
        }
    }

    void endTrigger(boolean start) {
        int n = getChildCount();
        for (int i = 0; i < n; i++) {
            final View child = getChildAt(i);
            final MotionController frame = mFrameArrayList.get(child);
            if (frame != null) {
                frame.endTrigger(start);
            }
        }
    }

    void evaluate(boolean force) {

        if (mTransitionLastTime == -1) {
            mTransitionLastTime = getNanoTime();
        }
        if (mTransitionLastPosition > 0.0f && mTransitionLastPosition < 1.0f) {
            mCurrentState = UNSET;
        }

        boolean newState = false;
        if (mKeepAnimating || (mInTransition
                && (force || mTransitionGoalPosition != mTransitionLastPosition))) {
            float dir = Math.signum(mTransitionGoalPosition - mTransitionLastPosition);
            long currentTime = getNanoTime();

            float deltaPos = 0f;
            if (!(mInterpolator instanceof MotionInterpolator)) { // if we are not in a drag
                deltaPos = dir * (currentTime - mTransitionLastTime) * 1E-9f / mTransitionDuration;
            }
            float position = mTransitionLastPosition + deltaPos;

            boolean done = false;
            if (mTransitionInstantly) {
                position = mTransitionGoalPosition;
            }

            if ((dir > 0 && position >= mTransitionGoalPosition)
                    || (dir <= 0 && position <= mTransitionGoalPosition)) {
                position = mTransitionGoalPosition;
                mInTransition = false;
                done = true;
            }
            if (DEBUG) {
                Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = "
                        + mTransitionLastPosition + " position = " + position);
            }
            mTransitionLastPosition = position;
            mTransitionPosition = position;
            mTransitionLastTime = currentTime;
            int notStopLogic = 0;
            int stopLogicContinue = 1;
            int stopLogicStop = 2;
            int stopLogicDone = notStopLogic;
            if (mInterpolator != null && !done) {
                if (mTemporalInterpolator) {
                    float time = (currentTime - mAnimationStartTime) * 1E-9f;
                    position = mInterpolator.getInterpolation(time);
                    if (mInterpolator == mStopLogic) {
                        boolean dp = mStopLogic.isStopped();
                        stopLogicDone = dp ? stopLogicStop : stopLogicContinue;
                    }

                    if (DEBUG) {
                        Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = "
                                + mTransitionLastPosition + " position = " + position);
                    }
                    mTransitionLastPosition = position;

                    mTransitionLastTime = currentTime;
                    if (mInterpolator instanceof MotionInterpolator) {
                        float lastVelocity = ((MotionInterpolator) mInterpolator).getVelocity();
                        mLastVelocity = lastVelocity;
                        if (Math.abs(lastVelocity) * mTransitionDuration <= EPSILON
                                && stopLogicDone == stopLogicStop) {
                            mInTransition = false;
                        }
                        if (lastVelocity > 0 && position >= 1.0f) {
                            mTransitionLastPosition = position = 1.0f;
                            mInTransition = false;
                        }
                        if (lastVelocity < 0 && position <= 0) {
                            mTransitionLastPosition = position = 0.0f;
                            mInTransition = false;
                        }
                    }

                } else {

                    float p2 = position;
                    position = mInterpolator.getInterpolation(position);
                    if (mInterpolator instanceof MotionInterpolator) {
                        mLastVelocity = ((MotionInterpolator) mInterpolator).getVelocity();
                    } else {
                        p2 = mInterpolator.getInterpolation(p2 + deltaPos);
                        mLastVelocity = dir * (p2 - position) / deltaPos;
                    }

                }
            } else {
                mLastVelocity = deltaPos;
            }
            if (Math.abs(mLastVelocity) > EPSILON) {
                setState(TransitionState.MOVING);
            }

            if (stopLogicDone != stopLogicContinue) {
                if ((dir > 0 && position >= mTransitionGoalPosition)
                        || (dir <= 0 && position <= mTransitionGoalPosition)) {
                    position = mTransitionGoalPosition;
                    mInTransition = false;
                }

                if (position >= 1.0f || position <= 0.0f) {
                    mInTransition = false;
                    setState(TransitionState.FINISHED);
                }
            }

            int n = getChildCount();
            mKeepAnimating = false;
            long time = getNanoTime();
            if (DEBUG) {
                Log.v(TAG, "LAYOUT frame.interpolate at " + position);
            }
            mPostInterpolationPosition = position;
            float interPos = mProgressInterpolator == null ? position
                    : mProgressInterpolator.getInterpolation(position);
            if (mProgressInterpolator != null) {
                mLastVelocity =
                        mProgressInterpolator
                                .getInterpolation(position + dir / mTransitionDuration);
                mLastVelocity -= mProgressInterpolator.getInterpolation(position);
            }
            for (int i = 0; i < n; i++) {
                final View child = getChildAt(i);
                final MotionController frame = mFrameArrayList.get(child);
                if (frame != null) {
                    mKeepAnimating |= frame.interpolate(child, interPos, time, mKeyCache);
                }
            }
            if (DEBUG) {
                Log.v(TAG, " interpolate " + Debug.getLocation() + " " + Debug.getName(this)
                        + " " + Debug.getName(getContext(), mBeginState) + " " + position);
            }

            boolean end = ((dir > 0 && position >= mTransitionGoalPosition)
                    || (dir <= 0 && position <= mTransitionGoalPosition));
            if (!mKeepAnimating && !mInTransition && end) {
                setState(TransitionState.FINISHED);
            }
            if (mMeasureDuringTransition) {
                requestLayout();
            }

            mKeepAnimating |= !end;

            // If we have hit the begin state begin state could be unset
            if (position <= 0 && mBeginState != UNSET) {
                if (mCurrentState != mBeginState) {
                    newState = true;
                    mCurrentState = mBeginState;
                    ConstraintSet set = mScene.getConstraintSet(mBeginState);
                    set.applyCustomAttributes(this);
                    setState(TransitionState.FINISHED);
                }
            }

            if (position >= 1.0) {
                if (DEBUG) {
                    Log.v(TAG, Debug.getLoc() + " ============= setting  to end "
                            + Debug.getName(getContext(), mEndState) + "  " + position);
                }
                if (mCurrentState != mEndState) {
                    newState = true;
                    mCurrentState = mEndState;
                    ConstraintSet set = mScene.getConstraintSet(mEndState);
                    set.applyCustomAttributes(this);
                    setState(TransitionState.FINISHED);
                }
            }

            if (mKeepAnimating || mInTransition) {
                invalidate();
            } else {
                if ((dir > 0 && position == 1) || (dir < 0 && position == 0)) {
                    setState(TransitionState.FINISHED);
                }
            }
            if (!mKeepAnimating && !mInTransition && ((dir > 0 && position == 1)
                    || (dir < 0 && position == 0))) {
                onNewStateAttachHandlers();
            }
        }
        if (mTransitionLastPosition >= 1.0f) {
            if (mCurrentState != mEndState) {
                newState = true;
            }
            mCurrentState = mEndState;
        } else if (mTransitionLastPosition <= 0.0f) {
            if (mCurrentState != mBeginState) {
                newState = true;
            }
            mCurrentState = mBeginState;
        }

        mNeedsFireTransitionCompleted |= newState;

        if (newState && !mInLayout) {
            requestLayout();
        }

        mTransitionPosition = mTransitionLastPosition;
    }

    private boolean mNeedsFireTransitionCompleted = false;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        mInLayout = true;
        try {
            if (DEBUG) {
                Log.v(TAG, " onLayout " + getProgress() + "  " + Debug.getLocation());
            }
            if (mScene == null) {
                super.onLayout(changed, left, top, right, bottom);
                return;
            }
            int w = right - left;
            int h = bottom - top;
            if (mLastLayoutWidth != w || mLastLayoutHeight != h) {
                rebuildScene();
                evaluate(true);
                if (DEBUG) {
                    Log.v(TAG, " onLayout  rebuildScene  " + Debug.getLocation());
                }
            }

            mLastLayoutWidth = w;
            mLastLayoutHeight = h;
            mOldWidth = w;
            mOldHeight = h;
        } finally {
            mInLayout = false;
        }
    }

    /**
     * block ConstraintLayout from handling layout description
     *
     * @param id
     */
    @Override
    protected void parseLayoutDescription(int id) {
        mConstraintLayoutSpec = null;
    }

    private void init(AttributeSet attrs) {
        IS_IN_EDIT_MODE = isInEditMode();
        if (attrs != null) {
            TypedArray a = getContext()
                    .obtainStyledAttributes(attrs, R.styleable.MotionLayout);
            final int count = a.getIndexCount();

            boolean apply = true;
            for (int i = 0; i < count; i++) {
                int attr = a.getIndex(i);
                if (attr == R.styleable.MotionLayout_layoutDescription) {
                    int n = a.getResourceId(attr, UNSET);
                    mScene = new MotionScene(getContext(), this, n);
                } else if (attr == R.styleable.MotionLayout_currentState) {
                    mCurrentState = a.getResourceId(attr, UNSET);
                } else if (attr == R.styleable.MotionLayout_motionProgress) {
                    mTransitionGoalPosition = a.getFloat(attr, 0.0f);
                    mInTransition = true;
                } else if (attr == R.styleable.MotionLayout_applyMotionScene) {
                    apply = a.getBoolean(attr, apply);
                } else if (attr == R.styleable.MotionLayout_showPaths) {
                    if (mDebugPath == 0) { // favor motionDebug
                        mDebugPath = a.getBoolean(attr, false) ? DEBUG_SHOW_PATH : 0;
                    }
                } else if (attr == R.styleable.MotionLayout_motionDebug) {
                    mDebugPath = a.getInt(attr, 0);
                }
            }
            a.recycle();
            if (mScene == null) {
                Log.e(TAG, "WARNING NO app:layoutDescription tag");
            }
            if (!apply) {
                mScene = null;
            }
        }
        if (mDebugPath != 0) {
            checkStructure();
        }
        if (mCurrentState == UNSET && mScene != null) {

            mCurrentState = mScene.getStartId();
            mBeginState = mScene.getStartId();
            if (DEBUG) {
                Log.v(TAG, " ============= init   end is "
                        + Debug.getName(getContext(), mEndState));
            }
            mEndState = mScene.getEndId();
            if (DEBUG) {
                Log.v(TAG, " ============= init setting end to "
                        + Debug.getName(getContext(), mEndState));
            }
        }
    }

    /**
     * Sets a motion scene to the layout. Subsequent calls to it will override the previous scene.
     */
    public void setScene(MotionScene scene) {
        mScene = scene;
        mScene.setRtl(isRtl());
        rebuildScene();
    }

    /**
     * Get the motion scene of the layout.
     * Warning! This gives you direct access to the internal
     * state of the MotionLayout making it easy
     * corrupt the state.
     * @return the motion scene
     */
    public MotionScene getScene() {
        return mScene;
    }

    private void checkStructure() {
        if (mScene == null) {
            Log.e(TAG, "CHECK: motion scene not set! set \"app:layoutDescription=\"@xml/file\"");
            return;
        }

        checkStructure(mScene.getStartId(), mScene.getConstraintSet(mScene.getStartId()));
        SparseIntArray startToEnd = new SparseIntArray();
        SparseIntArray endToStart = new SparseIntArray();
        for (MotionScene.Transition definedTransition : mScene.getDefinedTransitions()) {
            if (definedTransition == mScene.mCurrentTransition) {
                Log.v(TAG, "CHECK: CURRENT");
            }
            checkStructure(definedTransition);
            int startId = definedTransition.getStartConstraintSetId();
            int endId = definedTransition.getEndConstraintSetId();
            String startString = Debug.getName(getContext(), startId);
            String endString = Debug.getName(getContext(), endId);
            if (startToEnd.get(startId) == endId) {

                Log.e(TAG, "CHECK: two transitions with the same start and end "
                        + startString + "->" + endString);
            }
            if (endToStart.get(endId) == startId) {

                Log.e(TAG, "CHECK: you can't have reverse transitions"
                        + startString + "->" + endString);
            }
            startToEnd.put(startId, endId);
            endToStart.put(endId, startId);
            if (mScene.getConstraintSet(startId) == null) {
                Log.e(TAG, " no such constraintSetStart " + startString);
            }

            if (mScene.getConstraintSet(endId) == null) {
                Log.e(TAG, " no such constraintSetEnd " + startString);
            }
        }
    }

    private void checkStructure(int csetId, ConstraintSet set) {
        String setName = Debug.getName(getContext(), csetId);
        int size = getChildCount();
        for (int i = 0; i < size; i++) {
            View v = getChildAt(i);
            int id = v.getId();
            if (id == -1) {
                Log.w(TAG, "CHECK: " + setName + " ALL VIEWS SHOULD HAVE ID's "
                        + v.getClass().getName() + " does not!");
            }
            ConstraintSet.Constraint c = set.getConstraint(id);
            if (c == null) {
                Log.w(TAG, "CHECK: " + setName + " NO CONSTRAINTS for " + Debug.getName(v));
            }
        }
        int[] ids = set.getKnownIds();
        for (int i = 0; i < ids.length; i++) {
            int id = ids[i];
            String idString = Debug.getName(getContext(), id);
            if (null == findViewById(ids[i])) {
                Log.w(TAG, "CHECK: " + setName + " NO View matches id " + idString);
            }
            if (set.getHeight(id) == UNSET) {
                Log.w(TAG, "CHECK: " + setName + "(" + idString + ") no LAYOUT_HEIGHT");
            }
            if (set.getWidth(id) == UNSET) {
                Log.w(TAG, "CHECK: " + setName + "(" + idString + ") no LAYOUT_HEIGHT");
            }
        }
    }

    private void checkStructure(MotionScene.Transition transition) {
        if (DEBUG) {
            Log.v(TAG, "CHECK: transition = " + transition.debugString(getContext()));
            Log.v(TAG, "CHECK: transition.setDuration = " + transition.getDuration());
        }
        if (transition.getStartConstraintSetId() == transition.getEndConstraintSetId()) {
            Log.e(TAG, "CHECK: start and end constraint set should not be the same!");
        }
    }

    /**
     * Display the debugging information such as paths information
     *
     * @param debugMode integer representing various debug modes
     *
     */
    public void setDebugMode(int debugMode) {
        mDebugPath = debugMode;
        invalidate();
    }

    private RectF mBoundsCheck = new RectF();
    private View mRegionView = null;
    private Matrix mInverseMatrix = null;

    private boolean callTransformedTouchEvent(View view,
                                              MotionEvent event,
                                              float offsetX,
                                              float offsetY) {
        Matrix viewMatrix = view.getMatrix();

        if (viewMatrix.isIdentity()) {
            event.offsetLocation(offsetX, offsetY);
            boolean handled = view.onTouchEvent(event);
            event.offsetLocation(-offsetX, -offsetY);

            return handled;
        }

        MotionEvent transformedEvent = MotionEvent.obtain(event);

        transformedEvent.offsetLocation(offsetX, offsetY);

        if (mInverseMatrix == null) {
            mInverseMatrix = new Matrix();
        }

        viewMatrix.invert(mInverseMatrix);
        transformedEvent.transform(mInverseMatrix);

        boolean handled = view.onTouchEvent(transformedEvent);

        transformedEvent.recycle();

        return handled;
    }

    /**
     * Walk the view tree to see if a child view handles a touch event.
     *
     * @param x
     * @param y
     * @param view
     * @param event
     * @return
     */
    private boolean handlesTouchEvent(float x, float y, View view, MotionEvent event) {
        boolean handled = false;
        if (view instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) view;
            final int childCount = group.getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                View child = group.getChildAt(i);
                if (handlesTouchEvent(x + child.getLeft() - view.getScrollX(),
                        y + child.getTop() - view.getScrollY(),
                        child, event)) {
                    handled = true;
                    break;
                }
            }
        }

        if (!handled) {
            mBoundsCheck.set(x, y,
                    x + view.getRight() - view.getLeft(),
                    y + view.getBottom() - view.getTop());

            if (event.getAction() != MotionEvent.ACTION_DOWN
                    || mBoundsCheck.contains(event.getX(), event.getY())) {
                if (callTransformedTouchEvent(view, event, -x, -y)) {
                    handled = true;
                }
            }
        }

        return handled;
    }

    /**
     * Intercepts the touch event to correctly handle touch region id handover
     *
     * @param event
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (mScene == null || !mInteractionEnabled) {
            return false;
        }

        if (mScene.mViewTransitionController != null) {
            mScene.mViewTransitionController.touchEvent(event);
        }
        MotionScene.Transition currentTransition = mScene.mCurrentTransition;
        if (currentTransition != null && currentTransition.isEnabled()) {
            TouchResponse touchResponse = currentTransition.getTouchResponse();
            if (touchResponse != null) {
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    RectF region = touchResponse.getTouchRegion(this, new RectF());
                    if (region != null
                            && !region.contains(event.getX(), event.getY())) {
                        return false;
                    }
                }
                int regionId = touchResponse.getTouchRegionId();
                if (regionId != MotionScene.UNSET) {
                    if (mRegionView == null || mRegionView.getId() != regionId) {
                        mRegionView = findViewById(regionId);
                    }
                    if (mRegionView != null) {
                        mBoundsCheck.set(mRegionView.getLeft(),
                                mRegionView.getTop(),
                                mRegionView.getRight(),
                                mRegionView.getBottom());
                        if (mBoundsCheck.contains(event.getX(), event.getY())) {
                            // In case of region id, if the view or a child of the view
                            // handles an event we don't need to do anything;
                            if (!handlesTouchEvent(mRegionView.getLeft(), mRegionView.getTop(),
                                    mRegionView, event)) {
                                // but if not, then *we* need to handle the event.
                                return onTouchEvent(event);
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (DEBUG) {
            Log.v(TAG, Debug.getLocation() + " onTouchEvent = " + mTransitionLastPosition);
        }
        if (mScene != null && mInteractionEnabled && mScene.supportTouch()) {
            MotionScene.Transition currentTransition = mScene.mCurrentTransition;
            if (currentTransition != null && !currentTransition.isEnabled()) {
                return super.onTouchEvent(event);
            }
            mScene.processTouchEvent(event, getCurrentState(), this);
            if (mScene.mCurrentTransition.isTransitionFlag(TRANSITION_FLAG_INTERCEPT_TOUCH)) {
                return mScene.mCurrentTransition.getTouchResponse().isDragStarted();
            }
            return true;
        }
        if (DEBUG) {
            Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = "
                    + mTransitionLastPosition);
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        Display display = getDisplay();
        if (display != null) {
            mPreviouseRotation = display.getRotation();
        }
        if (mScene != null && mCurrentState != UNSET) {
            ConstraintSet cSet = mScene.getConstraintSet(mCurrentState);
            mScene.readFallback(this);
            if (mDecoratorsHelpers != null) {
                for (MotionHelper mh : mDecoratorsHelpers) {
                    mh.onFinishedMotionScene(this);
                }
            }
            if (cSet != null) {
                cSet.applyTo(this);
            }
            mBeginState = mCurrentState;
        }
        onNewStateAttachHandlers();
        if (mStateCache != null) {
            if (mDelayedApply) {
                post(new Runnable() {
                    @Override
                    public void run() {
                        mStateCache.apply();
                    }
                });
            } else {
                mStateCache.apply();
            }
        } else {
            if (mScene != null && mScene.mCurrentTransition != null) {
                if (mScene.mCurrentTransition.getAutoTransition()
                        == MotionScene.Transition.AUTO_ANIMATE_TO_END) {
                    transitionToEnd();
                    setState(TransitionState.SETUP);
                    setState(TransitionState.MOVING);
                }
            }
        }
    }

    @Override
    public void onRtlPropertiesChanged(int layoutDirection) {
        if (mScene != null) {
            mScene.setRtl(isRtl());
        }
    }

    /**
     * This function will set up various handlers (swipe, click...) whenever
     * a new state is reached.
     */
    void onNewStateAttachHandlers() {
        if (mScene == null) {
            return;
        }
        if (mScene.autoTransition(this, mCurrentState)) {
            requestLayout();
            return;
        }
        if (mCurrentState != UNSET) {
            mScene.addOnClickListeners(this, mCurrentState);
        }
        if (mScene.supportTouch()) {
            mScene.setupTouch();
        }
    }

    /**
     * Return the current state id
     *
     * @return current state id
     */
    public int getCurrentState() {
        return mCurrentState;
    }

    /**
     * Get current position during an animation.
     *
     * @return current position from 0.0 to 1.0 inclusive
     */
    public float getProgress() {
        return mTransitionLastPosition;
    }

    /**
     * Provide an estimate of the motion with respect to change in transitionPosition
     * (assume you are currently in a transition)
     *
     * @param mTouchAnchorId id of the anchor view that will be "moved" by touch
     * @param pos            the transition position at which to estimate the position
     * @param locationX      the x location within the view (0.0 = left , 1.0 = right)
     * @param locationY      the y location within the view (0.0 = left , 1.0 = right)
     * @param mAnchorDpDt    returns the dx/dp and dy/dp
     */
    void getAnchorDpDt(int mTouchAnchorId,
                       float pos,
                       float locationX, float locationY,
                       float[] mAnchorDpDt) {
        View v;
        MotionController f = mFrameArrayList.get(v = getViewById(mTouchAnchorId));
        if (DEBUG) {
            Log.v(TAG, " getAnchorDpDt " + Debug.getName(v) + " " + Debug.getLocation());
        }
        if (f != null) {
            f.getDpDt(pos, locationX, locationY, mAnchorDpDt);
            float y = v.getY();
            float deltaPos = pos - mLastPos;
            float deltaY = y - mLastY;
            @SuppressWarnings("unused")
            float dydp = (deltaPos != 0.0f) ? deltaY / deltaPos : Float.NaN;
            if (DEBUG) {
                Log.v(TAG, " getAnchorDpDt " + Debug.getName(v) + " "
                        + Debug.getLocation() + " " + Arrays.toString(mAnchorDpDt));
            }

            mLastPos = pos;
            mLastY = y;
        } else {
            String idName = (v == null) ? "" + mTouchAnchorId :
                    v.getContext().getResources().getResourceName(mTouchAnchorId);
            Log.w(TAG, "WARNING could not find view id " + idName);
        }
    }

    /**
     * Gets the time of the currently set animation.
     *
     * @return time in Milliseconds
     */
    public long getTransitionTimeMs() {
        if (mScene != null) {
            mTransitionDuration = mScene.getDuration() / 1000f;
        }
        return (long) (mTransitionDuration * 1000);
    }

    /**
     * Set a listener to be notified of drawer events.
     *
     * @param listener Listener to notify when drawer events occur
     * @see TransitionListener
     */
    public void setTransitionListener(TransitionListener listener) {
        mTransitionListener = listener;
    }

    /**
     * adds a listener to be notified of drawer events.
     *
     * @param listener Listener to notify when drawer events occur
     * @see TransitionListener
     */
    public void addTransitionListener(TransitionListener listener) {
        if (mTransitionListeners == null) {
            mTransitionListeners = new CopyOnWriteArrayList<>();
        }
        mTransitionListeners.add(listener);
    }

    /**
     * adds a listener to be notified of drawer events.
     *
     * @param listener Listener to notify when drawer events occur
     * @return <tt>true</tt> if it contained the specified listener
     * @see TransitionListener
     */
    public boolean removeTransitionListener(TransitionListener listener) {
        if (mTransitionListeners == null) {
            return false;
        }
        return mTransitionListeners.remove(listener);
    }

    /**
     * Listener for monitoring events about TransitionLayout. <b>Added in 2.0</b>
     */
    public interface TransitionListener {
        /**
         * Called when a drawer is about to start a transition.
         * Note. startId may be -1 if starting from an "undefined state"
         *
         * @param motionLayout The TransitionLayout view that was moved
         * @param startId      the id of the start state (or ConstraintSet). Will be -1 if unknown.
         * @param endId        the id of the end state (or ConstraintSet).
         */
        void onTransitionStarted(MotionLayout motionLayout,
                                        int startId, int endId);

        /**
         * Called when a drawer's position changes.
         *
         * @param motionLayout The TransitionLayout view that was moved
         * @param startId      the id of the start state (or ConstraintSet). Will be -1 if unknown.
         * @param endId        the id of the end state (or ConstraintSet).
         * @param progress     The progress on this transition, from 0 to 1.
         */
        void onTransitionChange(MotionLayout motionLayout,
                                int startId, int endId,
                                float progress);

        /**
         * Called when a drawer has settled completely a state.
         * The TransitionLayout is interactive at this point.
         *
         * @param motionLayout Drawer view that is now open
         * @param currentId    the id it has reached
         */
        void onTransitionCompleted(MotionLayout motionLayout, int currentId);

        /**
         * Call when a trigger is fired
         *
         * @param motionLayout
         * @param triggerId    The id set set with triggerID
         * @param positive     for positive transition edge
         * @param progress
         */
        void onTransitionTrigger(MotionLayout motionLayout, int triggerId, boolean positive,
                                 float progress);
    }

    /**
     * This causes the callback onTransitionTrigger to be called
     *
     * @param triggerId The id set set with triggerID
     * @param positive  for positive transition edge
     * @param progress  the current progress
     */
    public void fireTrigger(int triggerId, boolean positive, float progress) {
        if (mTransitionListener != null) {
            mTransitionListener.onTransitionTrigger(this, triggerId, positive, progress);
        }
        if (mTransitionListeners != null) {
            for (TransitionListener listeners : mTransitionListeners) {
                listeners.onTransitionTrigger(this, triggerId, positive, progress);
            }
        }
    }

    private void fireTransitionChange() {
        if (mTransitionListener != null
                || (mTransitionListeners != null && !mTransitionListeners.isEmpty())) {
            if (mListenerPosition != mTransitionPosition) {
                if (mListenerState != UNSET) {
                    fireTransitionStarted();
                    mIsAnimating = true;
                }
                mListenerState = UNSET;
                mListenerPosition = mTransitionPosition;
                if (mTransitionListener != null) {
                    mTransitionListener.onTransitionChange(this,
                            mBeginState, mEndState, mTransitionPosition);
                }
                if (mTransitionListeners != null) {
                    for (TransitionListener listeners : mTransitionListeners) {
                        listeners.onTransitionChange(this,
                                mBeginState, mEndState, mTransitionPosition);
                    }
                }
                mIsAnimating = true;
            }
        }
    }

    ArrayList<Integer> mTransitionCompleted = new ArrayList<>();

    /**
     * This causes the callback TransitionCompleted to be called
     */
    protected void fireTransitionCompleted() {
        if (mTransitionListener != null
                || (mTransitionListeners != null && !mTransitionListeners.isEmpty())) {
            if (mListenerState == UNSET) {
                mListenerState = mCurrentState;
                int lastState = UNSET;
                if (!mTransitionCompleted.isEmpty()) {
                    lastState = mTransitionCompleted.get(mTransitionCompleted.size() - 1);
                }
                if (lastState != mCurrentState && mCurrentState != -1) {
                    mTransitionCompleted.add(mCurrentState);
                }
            }
        }
        processTransitionCompleted();
        if (mOnComplete != null) {
            mOnComplete.run();
            mOnComplete = null;
        }

        if (mScheduledTransitionTo != null && mScheduledTransitions > 0) {
            transitionToState(mScheduledTransitionTo[0]);
            System.arraycopy(mScheduledTransitionTo,
                    1, mScheduledTransitionTo,
                    0, mScheduledTransitionTo.length - 1);
            mScheduledTransitions--;
        }
    }

    private void processTransitionCompleted() {
        if (mTransitionListener == null
                && (mTransitionListeners == null || mTransitionListeners.isEmpty())) {
            return;
        }
        mIsAnimating = false;
        for (Integer state : mTransitionCompleted) {
            if (mTransitionListener != null) {
                mTransitionListener.onTransitionCompleted(this, state);
            }
            if (mTransitionListeners != null) {
                for (TransitionListener listeners : mTransitionListeners) {
                    listeners.onTransitionCompleted(this, state);
                }
            }
        }
        mTransitionCompleted.clear();
    }

    /**
     *
     */
    public DesignTool getDesignTool() {
        if (mDesignTool == null) {
            mDesignTool = new DesignTool(this);
        }
        return mDesignTool;
    }

    /**
     *
     */
    @Override
    public void onViewAdded(View view) {
        super.onViewAdded(view);
        if (view instanceof MotionHelper) {
            MotionHelper helper = (MotionHelper) view;
            if (mTransitionListeners == null) {
                mTransitionListeners = new CopyOnWriteArrayList<>();
            }
            mTransitionListeners.add(helper);

            if (helper.isUsedOnShow()) {
                if (mOnShowHelpers == null) {
                    mOnShowHelpers = new ArrayList<>();
                }
                mOnShowHelpers.add(helper);
            }
            if (helper.isUseOnHide()) {
                if (mOnHideHelpers == null) {
                    mOnHideHelpers = new ArrayList<>();
                }
                mOnHideHelpers.add(helper);
            }
            if (helper.isDecorator()) {
                if (mDecoratorsHelpers == null) {
                    mDecoratorsHelpers = new ArrayList<>();
                }
                mDecoratorsHelpers.add(helper);
            }
        }
    }

    /**
     *
     */
    @Override
    public void onViewRemoved(View view) {
        super.onViewRemoved(view);
        if (mOnShowHelpers != null) {
            mOnShowHelpers.remove(view);
        }
        if (mOnHideHelpers != null) {
            mOnHideHelpers.remove(view);
        }
    }

    /**
     * Notify OnShow motion helpers
     * @param progress
     */
    public void setOnShow(float progress) {
        if (mOnShowHelpers != null) {
            final int count = mOnShowHelpers.size();
            for (int i = 0; i < count; i++) {
                MotionHelper helper = mOnShowHelpers.get(i);
                helper.setProgress(progress);
            }
        }
    }

    /**
     * Notify OnHide motion helpers
     * @param progress
     */
    public void setOnHide(float progress) {
        if (mOnHideHelpers != null) {
            final int count = mOnHideHelpers.size();
            for (int i = 0; i < count; i++) {
                MotionHelper helper = mOnHideHelpers.get(i);
                helper.setProgress(progress);
            }
        }
    }

    /**
     * Get the id's of all constraintSets used by MotionLayout
     *
     * @return
     */
    public  @IdRes
            int[] getConstraintSetIds() {
        if (mScene == null) {
            return null;
        }
        return mScene.getConstraintSetIds();
    }

    /**
     * Get the id's of all constraintSets with the matching types
     *
     * @return
     */
    public int[] getMatchingConstraintSetIds(String ... types) {
        if (mScene == null) {
            return null;
        }
        return mScene.getMatchingStateLabels(types);
    }

    /**
     * Get the ConstraintSet associated with an id
     * This returns a link to the constraintSet
     * But in most cases can be used.
     * createConstraintSet makes a copy which is more expensive.
     *
     * @param id of the constraintSet
     * @return ConstraintSet of MotionLayout
     * @see #cloneConstraintSet(int)
     */
    public ConstraintSet getConstraintSet(int id) {
        if (mScene == null) {
            return null;
        }
        return mScene.getConstraintSet(id);
    }

    /**
     * Creates a ConstraintSet based on an existing
     * constraintSet.
     * This makes a copy of the ConstraintSet.
     *
     * @param id The ide of the ConstraintSet
     * @return the ConstraintSet
     */
    public ConstraintSet cloneConstraintSet(int id) {
        if (mScene == null) {
            return null;
        }
        ConstraintSet orig = mScene.getConstraintSet(id);
        ConstraintSet ret = new ConstraintSet();
        ret.clone(orig);
        return ret;
    }

    /**
     * rebuild the motion Layouts
     *
     * @deprecated Please call rebuildScene() instead.
     */
    @Deprecated
    public void rebuildMotion() {
        Log.e(TAG, "This method is deprecated. Please call rebuildScene() instead.");
        rebuildScene();
    }

    /**
     * rebuild the motion Layouts
     */
    public void rebuildScene() {
        mModel.reEvaluateState();
        invalidate();
    }

    /**
     * update a ConstraintSet under the id.
     *
     * @param stateId id of the ConstraintSet
     * @param set     The constraintSet
     */
    public void updateState(int stateId, ConstraintSet set) {
        if (mScene != null) {
            mScene.setConstraintSet(stateId, set);
        }
        updateState();
        if (mCurrentState == stateId) {
            set.applyTo(this);
        }
    }

    /**
     * Update a ConstraintSet but animate the change.
     *
     * @param stateId  id of the ConstraintSet
     * @param set      The constraintSet
     * @param duration The length of time to perform the animation
     */
    public void updateStateAnimate(int stateId, ConstraintSet set, int duration) {
        if (mScene == null) {
            return;
        }

        if (mCurrentState == stateId) {
            updateState(R.id.view_transition, getConstraintSet(stateId));
            setState(R.id.view_transition, -1, -1);
            updateState(stateId, set);
            MotionScene.Transition tmpTransition =
                    new MotionScene.Transition(-1, mScene, R.id.view_transition, stateId);
            tmpTransition.setDuration(duration);
            setTransition(tmpTransition);
            transitionToEnd();
        }
    }

    /**
     * on completing the current transition, transition to this state.
     *
     * @param id
     */
    public void scheduleTransitionTo(int id) {
        if (getCurrentState() == -1) {
            transitionToState(id);
        } else {
            if (mScheduledTransitionTo == null) {
                mScheduledTransitionTo = new int[4];
            } else if (mScheduledTransitionTo.length <= mScheduledTransitions) {
                mScheduledTransitionTo =
                        Arrays.copyOf(mScheduledTransitionTo, mScheduledTransitionTo.length * 2);
            }
            mScheduledTransitionTo[mScheduledTransitions++] = id;
        }
    }

    /**
     * Not sure we want this
     *
     *
     */
    public void updateState() {
        mModel.initFrom(mLayoutWidget,
                mScene.getConstraintSet(mBeginState),
                mScene.getConstraintSet(mEndState));
        rebuildScene();
    }

    /**
     * Get all Transitions known to the system.
     *
     * @return
     */
    public ArrayList<MotionScene.Transition> getDefinedTransitions() {
        if (mScene == null) {
            return null;
        }
        return mScene.getDefinedTransitions();
    }

    /**
     * Gets the state you are currently transitioning from.
     * If you are transitioning from an unknown state returns -1
     *
     * @return State you are transitioning from.
     */
    public int getStartState() {
        return mBeginState;
    }

    /**
     * Gets the state you are currently transition to.
     *
     * @return The State you are transitioning to.
     */
    public int getEndState() {
        return mEndState;
    }

    /**
     * Gets the position you are animating to typically 0 or 1.
     * This is useful during animation after touch up
     *
     * @return The target position you are moving to
     */
    public float getTargetPosition() {
        return mTransitionGoalPosition;
    }

    /**
     * Change the current Transition duration.
     *
     * @param milliseconds duration for transition to complete
     */
    public void setTransitionDuration(int milliseconds) {
        if (mScene == null) {
            Log.e(TAG, "MotionScene not defined");
            return;
        }
        mScene.setDuration(milliseconds);
    }

    /**
     * This returns the internal Transition Structure
     *
     * @param id
     * @return
     */
    public MotionScene.Transition getTransition(int id) {
        return mScene.getTransitionById(id);
    }

    /**
     * This looks up the constraintset ID given an id string (
     *
     * @param id String id (without the "@+id/")
     * @return the integer id of the string
     *
     */
    int lookUpConstraintId(String id) {
        if (mScene == null) {
            return 0;
        }
        return mScene.lookUpConstraintId(id);
    }

    /**
     * does a revers look up to find the ConstraintSets Name
     *
     * @param id the integer id of the constraintSet
     * @return
     */
    String getConstraintSetNames(int id) {
        if (mScene == null) {
            return null;
        }
        return mScene.lookUpConstraintName(id);
    }

    /**
     * this allow disabling autoTransitions to prevent design surface from being in undefined states
     *
     * @param disable
     */
    void disableAutoTransition(boolean disable) {
        if (mScene == null) {
            return;
        }
        mScene.disableAutoTransition(disable);
    }

    /**
     * Enables (or disables) MotionLayout's onClick and onSwipe handling.
     *
     * @param enabled If true,  touch & click  is enabled; otherwise it is disabled
     */
    public void setInteractionEnabled(boolean enabled) {
        mInteractionEnabled = enabled;
    }

    /**
     * Determines whether MotionLayout's touch & click handling are enabled.
     * An interaction enabled MotionLayout can respond to user input and initiate and control.
     * MotionLayout interactions are enabled initially by default.
     * MotionLayout touch & click handling may be enabled or disabled by calling its
     * setInteractionEnabled method.
     *
     * @return true if MotionLayout's  touch & click  is enabled, false otherwise
     */
    public boolean isInteractionEnabled() {
        return mInteractionEnabled;
    }

    private void fireTransitionStarted() {
        if (mTransitionListener != null) {
            mTransitionListener.onTransitionStarted(this, mBeginState, mEndState);
        }
        if (mTransitionListeners != null) {
            for (TransitionListener listeners : mTransitionListeners) {
                listeners.onTransitionStarted(this, mBeginState, mEndState);
            }
        }
    }

    /**
     * Execute a ViewTransition.
     * Transition will execute if its conditions are met and it is enabled
     *
     * @param viewTransitionId
     * @param view             The views to apply to
     */
    public void viewTransition(int viewTransitionId, View... view) {
        if (mScene != null) {
            mScene.viewTransition(viewTransitionId, view);
        } else {
            Log.e(TAG, " no motionScene");
        }
    }

    /**
     * Enable a ViewTransition ID.
     *
     * @param viewTransitionId id of ViewTransition
     * @param enable           If false view transition cannot be executed.
     */
    public void enableViewTransition(int viewTransitionId, boolean enable) {
        if (mScene != null) {
            mScene.enableViewTransition(viewTransitionId, enable);
        }
    }

    /**
     * Is transition id enabled or disabled
     *
     * @param viewTransitionId the ide of the transition
     * @return true if enabled
     */
    public boolean isViewTransitionEnabled(int viewTransitionId) {
        if (mScene != null) {
            return mScene.isViewTransitionEnabled(viewTransitionId);
        }
        return false;
    }

    /**
     * Apply the view transitions keyFrames to the MotionController.
     * Note ConstraintOverride is not used
     *
     * @param viewTransitionId the id of the view transition
     * @param motionController the MotionController to apply the keyframes to
     * @return true if it found and applied the viewTransition false otherwise
     */
    public boolean applyViewTransition(int viewTransitionId, MotionController motionController) {
        if (mScene != null) {
            return mScene.applyViewTransition(viewTransitionId, motionController);
        }
        return false;
    }

    /**
     * Is initial state changes are applied during onAttachedToWindow or after.
     * @return
     */
    public boolean isDelayedApplicationOfInitialState() {
        return mDelayedApply;
    }

    /**
     * Initial state changes are applied during onAttachedToWindow unless this is set to true.
     * @param delayedApply
     */
    public void setDelayedApplicationOfInitialState(boolean delayedApply) {
        this.mDelayedApply = delayedApply;
    }

}