MotionLayout.java

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

package androidx.constraintlayout.motion.widget;

import 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.annotation.RequiresApi;
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 androidx.core.view.ViewCompat;

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>see {@see androidx.constraintlayout.widget.
 * ConstraintLayout ConstraintLayout} 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
     *
     * @return
     */
    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);
            int current = mCurrentState;
            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 (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT || isAttachedToWindow()) {
                    try {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                            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;
        }
    }

    /**
     * Returns true if the provided view is currently attached to a window.
     */
    @Override
    public boolean isAttachedToWindow() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return super.isAttachedToWindow();
        }
        return getWindowToken() != 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 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 don
     */
    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
     */
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    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);
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    layoutParams.resolveLayoutDirection(getLayoutDirection());
                } else {
                    layoutParams.resolveLayoutDirection(ViewCompat.LAYOUT_DIRECTION_LTR);
                }
                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;
            int optimisationLevel = getOptimizationLevel();

            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();

                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
     * @DoNotShow
     */
    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();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            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;
            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) {
                    if (mTransitionListener != null) {
                        mTransitionListener.onTransitionStarted(this, mBeginState, mEndState);
                    }
                    if (mTransitionListeners != null) {
                        for (TransitionListener listeners : mTransitionListeners) {
                            listeners.onTransitionStarted(this, mBeginState, mEndState);
                        }
                    }
                    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();
        }

        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();
    }

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

    /**
     * @DoNotShow
     */
    @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);
            }
        }
    }

    /**
     * @DoNotShow
     */
    @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
     *
     * @DoNotShow
     */
    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
     * @DoNotShow
     */
    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(MotionLayout motionLayout, int mBeginState, int mEndState) {
        if (mTransitionListener != null) {
            mTransitionListener.onTransitionStarted(this, mBeginState, mEndState);
        }
        if (mTransitionListeners != null) {
            for (TransitionListener listeners : mTransitionListeners) {
                listeners.onTransitionStarted(motionLayout, 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;
    }

}