content: /*
   * 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
       */
      protected interface MotionTracker {
          void recycle();
  
          void clear();
  
          void addMovement(MotionEvent event);
  
          void computeCurrentVelocity(int units);
  
          void computeCurrentVelocity(int units, float maxVelocity);
  
          float getXVelocity();
  
          float getYVelocity();
  
          float getXVelocity(int id);
  
          float getYVelocity(int id);
      }
  
      void setState(TransitionState newState) {
          if (DEBUG) {
              Debug.logStack(TAG, mTransitionState + " -> " + newState + " "
                      + Debug.getName(getContext(), mCurrentState), 2);
          }
          if (newState == TransitionState.FINISHED && mCurrentState == UNSET) {
              return;
          }
          TransitionState oldState = mTransitionState;
          mTransitionState = newState;
  
          if (oldState == TransitionState.MOVING && newState == TransitionState.MOVING) {
              fireTransitionChange();
          }
          switch (oldState) {
              case UNDEFINED:
              case SETUP:
                  if (newState == TransitionState.MOVING) {
                      fireTransitionChange();
                  }
                  if (newState == TransitionState.FINISHED) {
                      fireTransitionCompleted();
                  }
                  break;
              case MOVING:
                  if (newState == TransitionState.FINISHED) {
                      fireTransitionCompleted();
                  }
                  break;
              case FINISHED:
                  break;
          }
      }
  
      private static class MyTracker implements MotionTracker {
          VelocityTracker mTracker;
          private static MyTracker sMe = new MyTracker();
  
          public static MyTracker obtain() {
              sMe.mTracker = VelocityTracker.obtain();
              return sMe;
          }
  
          @Override
          public void recycle() {
              if (mTracker != null) {
                  mTracker.recycle();
                  mTracker = null; // not allowed to call after recycle
              }
          }
  
          @Override
          public void clear() {
              if (mTracker != null) {
                  mTracker.clear();
              }
          }
  
          @Override
          public void addMovement(MotionEvent event) {
              if (mTracker != null) {
                  mTracker.addMovement(event);
              }
          }
  
          @Override
          public void computeCurrentVelocity(int units) {
              if (mTracker != null) {
                  mTracker.computeCurrentVelocity(units);
              }
          }
  
          @Override
          public void computeCurrentVelocity(int units, float maxVelocity) {
              if (mTracker != null) {
                  mTracker.computeCurrentVelocity(units, maxVelocity);
              }
          }
  
          @Override
          public float getXVelocity() {
              if (mTracker != null) {
                  return mTracker.getXVelocity();
              }
              return 0;
          }
  
          @Override
          public float getYVelocity() {
              if (mTracker != null) {
                  return mTracker.getYVelocity();
              }
              return 0;
          }
  
          @Override
          public float getXVelocity(int id) {
              if (mTracker != null) {
                  return mTracker.getXVelocity(id);
              }
              return 0;
          }
  
          @Override
          public float getYVelocity(int id) {
              if (mTracker != null) {
                  return getYVelocity(id);
              }
              return 0;
          }
      }
  
      /**
       * sets the state to start in. To be used during OnCreate
       *
       * @param beginId the id of the start constraint set
       */
      void setStartState(int beginId) {
          if (!isAttachedToWindow()) {
              if (mStateCache == null) {
                  mStateCache = new StateCache();
              }
              mStateCache.setStartState(beginId);
              mStateCache.setEndState(beginId);
              return;
          }
          mCurrentState = beginId;
      }
  
      /**
       * Set a transition explicitly between two constraint sets
       *
       * @param beginId the id of the start constraint set
       * @param endId   the id of the end constraint set
       */
      public void setTransition(int beginId, int endId) {
          if (!isAttachedToWindow()) {
              if (mStateCache == null) {
                  mStateCache = new StateCache();
              }
              mStateCache.setStartState(beginId);
              mStateCache.setEndState(endId);
              return;
          }
  
          if (mScene != null) {
              mBeginState = beginId;
              mEndState = endId;
              if (DEBUG) {
                  Log.v(TAG, Debug.getLocation() + " setTransition "
                          + Debug.getName(getContext(), beginId) + " -> "
                          + Debug.getName(getContext(), endId));
              }
              mScene.setTransition(beginId, endId);
              mModel.initFrom(mLayoutWidget, mScene.getConstraintSet(beginId),
                      mScene.getConstraintSet(endId));
              rebuildScene();
              mTransitionLastPosition = 0;
              transitionToStart();
          }
      }
  
      /**
       * Set a transition explicitly to a Transition that has an ID
       * The transition must have been named with android:id=...
       *
       * @param transitionId the id to set
       */
      public void setTransition(int transitionId) {
          if (mScene != null) {
              MotionScene.Transition transition = getTransition(transitionId);
              mBeginState = transition.getStartConstraintSetId();
              mEndState = transition.getEndConstraintSetId();
  
              if (!isAttachedToWindow()) {
                  if (mStateCache == null) {
                      mStateCache = new StateCache();
                  }
                  mStateCache.setStartState(mBeginState);
                  mStateCache.setEndState(mEndState);
                  return;
              }
  
              if (DEBUG) {
                  Log.v(TAG, Debug.getLocation() + " setTransition "
                          + Debug.getName(getContext(), mBeginState) + " -> "
                          + Debug.getName(getContext(), mEndState)
                          + "   current=" + Debug.getName(getContext(), mCurrentState));
              }
  
              float pos = Float.NaN;
              if (mCurrentState == mBeginState) {
                  pos = 0;
              } else if (mCurrentState == mEndState) {
                  pos = 1;
              }
              mScene.setTransition(transition);
              mModel.initFrom(mLayoutWidget,
                      mScene.getConstraintSet(mBeginState),
                      mScene.getConstraintSet(mEndState));
              rebuildScene();
  
              if (mTransitionLastPosition != pos) {
                  // If the last drawn position isn't the same,
                  // we might have to make sure we apply the corresponding constraintset.
                  if (pos == 0) {
                      endTrigger(true);
                      mScene.getConstraintSet(mBeginState).applyTo(this);
                  } else if (pos == 1) {
                      endTrigger(false);
                      mScene.getConstraintSet(mEndState).applyTo(this);
                  }
              }
  
              mTransitionLastPosition = Float.isNaN(pos) ? 0 : pos;
  
              if (Float.isNaN(pos)) {
                  Log.v(TAG, Debug.getLocation() + " transitionToStart ");
                  transitionToStart();
              } else {
                  setProgress(pos);
              }
          }
      }
  
      protected void setTransition(MotionScene.Transition transition) {
          mScene.setTransition(transition);
          setState(TransitionState.SETUP);
          if (mCurrentState == mScene.getEndId()) {
              mTransitionLastPosition = 1.0f;
              mTransitionPosition = 1.0f;
              mTransitionGoalPosition = 1;
          } else {
              mTransitionLastPosition = 0;
              mTransitionPosition = 0f;
              mTransitionGoalPosition = 0;
          }
          mTransitionLastTime =
                  transition.isTransitionFlag(TRANSITION_FLAG_FIRST_DRAW) ? -1 : getNanoTime();
          if (DEBUG) {
              Log.v(TAG, Debug.getLocation() + "  new mTransitionLastPosition = "
                      + mTransitionLastPosition + "");
              Log.v(TAG, Debug.getLocation() + " setTransition was "
                      + Debug.getName(getContext(), mBeginState)
                      + " -> " + Debug.getName(getContext(), mEndState));
          }
          int newBeginState = mScene.getStartId();
          int newEndState = mScene.getEndId();
          if (newBeginState == mBeginState && newEndState == mEndState) {
              return;
          }
          mBeginState = newBeginState;
          mEndState = newEndState;
          mScene.setTransition(mBeginState, mEndState);
  
          if (DEBUG) {
              Log.v(TAG, Debug.getLocation() + " setTransition now "
                      + Debug.getName(getContext(), mBeginState) + " -> "
                      + Debug.getName(getContext(), mEndState));
          }
  
          mModel.initFrom(mLayoutWidget,
                  mScene.getConstraintSet(mBeginState),
                  mScene.getConstraintSet(mEndState));
          mModel.setMeasuredId(mBeginState, mEndState);
          mModel.reEvaluateState();
  
          rebuildScene();
      }
  
      /**
       * This overrides ConstraintLayout and only accepts a MotionScene.
       *
       * @param motionScene The resource id, or 0 to reset the MotionScene.
       */
      @Override
      public void loadLayoutDescription(int motionScene) {
          if (motionScene != 0) {
              try {
                  mScene = new MotionScene(getContext(), this, motionScene);
                  if (mCurrentState == UNSET && mScene != null) {
                      mCurrentState = mScene.getStartId();
                      mBeginState = mScene.getStartId();
                      mEndState = mScene.getEndId();
                  }
                  if (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 starting position of the current transition.
       * This will not work during on create as there is no transition
       * Transitions are only set up during onAttach
       *
       * @param onComplete callback when task is done
       */
      public void transitionToStart(Runnable onComplete) {
          animateTo(0.0f);
          mOnComplete = onComplete;
      }
  
      /**
       * Animate to the ending position of the current transition.
       * This will not work during on create as there is no transition
       * Transitions are only set up during onAttach
       */
      public void transitionToEnd() {
          animateTo(1.0f);
          mOnComplete = null;
      }
  
      /**
       * Animate to the ending position of the current transition.
       * This will not work during on create as there is no transition
       * Transitions are only set up during onAttach
       *
       * @param onComplete callback when task is done
       */
      public void transitionToEnd(Runnable onComplete) {
          animateTo(1.0f);
          mOnComplete = onComplete;
      }
  
      /**
       * Animate to the state defined by the id.
       * The id is the id of the ConstraintSet or the id of the State.
       *
       * @param id the state to transition to
       */
      public void transitionToState(int id) {
          if (!isAttachedToWindow()) {
              if (mStateCache == null) {
                  mStateCache = new StateCache();
              }
              mStateCache.setEndState(id);
              return;
          }
          transitionToState(id, -1, -1);
      }
  
      /**
       * Animate to the state defined by the id.
       * The id is the id of the ConstraintSet or the id of the State.
       *
       * @param id       the state to transition to
       * @param duration time in ms. if 0 set by default or transition -1 by current
       */
  
      public void transitionToState(int id, int duration) {
          if (!isAttachedToWindow()) {
              if (mStateCache == null) {
                  mStateCache = new StateCache();
              }
              mStateCache.setEndState(id);
              return;
          }
          transitionToState(id, -1, -1, duration);
      }
  
      /**
       * Animate to the state defined by the id.
       * Width and height may be used in the picking of the id using this StateSet.
       *
       * @param id           the state to transition
       * @param screenWidth  the with of the motionLayout used to select the variant
       * @param screenHeight the height of the motionLayout used to select the variant
       */
      public void transitionToState(int id, int screenWidth, int screenHeight) {
          transitionToState(id, screenWidth, screenHeight, -1);
      }
  
      /**
       * Rotate the layout based on the angle to a ConstraintSet
       * @param id constraintSet
       * @param duration time to take to rotate
       */
      @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;
  
              computeStartEndSize(widthMeasureSpec, heightMeasureSpec);
  
              // This works around the problem that MotionLayout calls its children
              // Wrap content children
              // with measure(AT_MOST,AT_MOST) then measure(EXACTLY, EXACTLY)
              // if a child of MotionLayout is a motionLayout
              // it would not know it could resize during animation
              // other Layouts may have this behaviour but for now this is the only one we support
  
              boolean recompute_start_end_size = true;
              if (getParent() instanceof MotionLayout
                      && widthMode == MeasureSpec.EXACTLY
                      && heightMode == MeasureSpec.EXACTLY) {
                  recompute_start_end_size = false;
              }
              if (recompute_start_end_size) {
                  computeStartEndSize(widthMeasureSpec, heightMeasureSpec);
  
                  mStartWrapWidth = mLayoutStart.getWidth();
                  mStartWrapHeight = mLayoutStart.getHeight();
                  mEndWrapWidth = mLayoutEnd.getWidth();
                  mEndWrapHeight = mLayoutEnd.getHeight();
                  mMeasureDuringTransition = ((mStartWrapWidth != mEndWrapWidth)
                          || (mStartWrapHeight != mEndWrapHeight));
              }
  
              int width = mStartWrapWidth;
              int height = mStartWrapHeight;
              if (mWidthMeasureMode == MeasureSpec.AT_MOST
                      || mWidthMeasureMode == MeasureSpec.UNSPECIFIED) {
                  width = (int) (mStartWrapWidth + mPostInterpolationPosition
                          * (mEndWrapWidth - mStartWrapWidth));
              }
              if (mHeightMeasureMode == MeasureSpec.AT_MOST
                      || mHeightMeasureMode == MeasureSpec.UNSPECIFIED) {
                  height = (int) (mStartWrapHeight + mPostInterpolationPosition
                          * (mEndWrapHeight - mStartWrapHeight));
              }
  
              boolean isWidthMeasuredTooSmall = mLayoutStart.isWidthMeasuredTooSmall()
                      || mLayoutEnd.isWidthMeasuredTooSmall();
              boolean isHeightMeasuredTooSmall = mLayoutStart.isHeightMeasuredTooSmall()
                      || mLayoutEnd.isHeightMeasuredTooSmall();
              resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec,
                      width, height, isWidthMeasuredTooSmall, isHeightMeasuredTooSmall);
  
              if (DEBUG) {
                  Debug.logStack(TAG, ">>>>>>>>", 3);
                  debugLayout(">>>>>>> measure str ", mLayoutStart);
                  debugLayout(">>>>>>> measure end ", mLayoutEnd);
              }
          }
  
          private void computeStartEndSize(int widthMeasureSpec, int heightMeasureSpec) {
              int optimisationLevel = getOptimizationLevel();
  
              if (mCurrentState == getStartState()) {
                  resolveSystem(mLayoutEnd, optimisationLevel,
                          (mEnd == null || mEnd.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec,
                          (mEnd == null || mEnd.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec);
                  if (mStart != null) {
                      resolveSystem(mLayoutStart, optimisationLevel,
                              (mStart.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec,
                              (mStart.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec);
                  }
              } else {
                  if (mStart != null) {
                      resolveSystem(mLayoutStart, optimisationLevel,
                              (mStart.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec,
                              (mStart.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec);
                  }
                  resolveSystem(mLayoutEnd, optimisationLevel,
                          (mEnd == null || mEnd.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec,
                          (mEnd == null || mEnd.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec);
              }
          }
  
          public void build() {
              final int n = getChildCount();
              mFrameArrayList.clear();
              SparseArray<MotionController> controllers = new SparseArray<>();
              int[] ids = new int[n];
              for (int i = 0; i < n; i++) {
                  View v = getChildAt(i);
                  MotionController motionController = new MotionController(v);
                  controllers.put(ids[i] = v.getId(), motionController);
                  mFrameArrayList.put(v, motionController);
              }
              for (int i = 0; i < n; i++) {
                  View v = getChildAt(i);
                  MotionController motionController = mFrameArrayList.get(v);
                  if (motionController == null) {
                      continue;
                  }
                  if (mStart != null) {
                      ConstraintWidget startWidget = getWidget(mLayoutStart, v);
                      if (startWidget != null) {
                          motionController.setStartState(toRect(startWidget), mStart,
                                  getWidth(), getHeight());
                      } else {
                          if (mDebugPath != 0) {
                              Log.e(TAG, Debug.getLocation() + "no widget for  "
                                      + Debug.getName(v) + " (" + v.getClass().getName() + ")");
                          }
                      }
                  } else {
                      if (mInRotation) {
                          motionController.setStartState(mPreRotate.get(v), v, mRotatMode,
                                  mPreRotateWidth, mPreRotateHeight);
                      }
                  }
                  if (mEnd != null) {
                      ConstraintWidget endWidget = getWidget(mLayoutEnd, v);
                      if (endWidget != null) {
                          motionController.setEndState(toRect(endWidget), mEnd,
                                  getWidth(), getHeight());
                      } else {
                          if (mDebugPath != 0) {
                              Log.e(TAG, Debug.getLocation() + "no widget for  "
                                      + Debug.getName(v)
                                      + " (" + v.getClass().getName() + ")");
                          }
                      }
                  }
              }
  
              for (int i = 0; i < n; i++) {
                  MotionController controller = controllers.get(ids[i]);
                  int relativeToId = controller.getAnimateRelativeTo();
                  if (relativeToId != UNSET) {
                      controller.setupRelative(controllers.get(relativeToId));
                  }
              }
          }
  
          public void setMeasuredId(int startId, int endId) {
              mStartId = startId;
              mEndId = endId;
          }
  
          public boolean isNotConfiguredWith(int startId, int endId) {
              return startId != mStartId || endId != mEndId;
          }
      }
  
      Model mModel = new Model();
  
      private Rect toRect(ConstraintWidget cw) {
          mTempRect.top = cw.getY();
          mTempRect.left = cw.getX();
          mTempRect.right = cw.getWidth() + mTempRect.left;
          mTempRect.bottom = cw.getHeight() + mTempRect.top;
          return mTempRect;
      }
  
      @Override
      public void requestLayout() {
          if (!mMeasureDuringTransition) {
              if (mCurrentState == UNSET && mScene != null
                      && mScene.mCurrentTransition != null) {
                  int mode = mScene.mCurrentTransition.getLayoutDuringTransition();
                  if (mode == MotionScene.LAYOUT_IGNORE_REQUEST) {
                      return;
                  } else if (mode == MotionScene.LAYOUT_CALL_MEASURE) {
                      final int n = getChildCount();
                      for (int i = 0; i < n; i++) {
                          View v = getChildAt(i);
                          MotionController motionController = mFrameArrayList.get(v);
                          motionController.remeasure();
                      }
                      return;
                  }
              }
          }
          super.requestLayout();
      }
  
      @Override
      public String toString() {
          Context context = getContext();
          return Debug.getName(context, mBeginState) + "->"
                  + Debug.getName(context, mEndState)
                  + " (pos:" + mTransitionLastPosition + " Dpos/Dt:" + mLastVelocity;
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          if (DEBUG) {
              Log.v(TAG, "onMeasure " + Debug.getLocation());
          }
          if (mScene == null) {
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
              return;
          }
          boolean recalc = (mLastWidthMeasureSpec != widthMeasureSpec
                  || mLastHeightMeasureSpec != heightMeasureSpec);
          if (mNeedsFireTransitionCompleted) {
              mNeedsFireTransitionCompleted = false;
              onNewStateAttachHandlers();
              processTransitionCompleted();
              recalc = true;
          }
  
          if (mDirtyHierarchy) {
              recalc = true;
          }
  
          mLastWidthMeasureSpec = widthMeasureSpec;
          mLastHeightMeasureSpec = heightMeasureSpec;
  
          int startId = mScene.getStartId();
          int endId = mScene.getEndId();
          boolean setMeasure = true;
          if ((recalc || mModel.isNotConfiguredWith(startId, endId)) && mBeginState != UNSET) {
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
              mModel.initFrom(mLayoutWidget, mScene.getConstraintSet(startId),
                      mScene.getConstraintSet(endId));
              mModel.reEvaluateState();
              mModel.setMeasuredId(startId, endId);
              setMeasure = false;
          } else if (recalc) {
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          }
  
          if (mMeasureDuringTransition || setMeasure) {
              int heightPadding = getPaddingTop() + getPaddingBottom();
              int widthPadding = getPaddingLeft() + getPaddingRight();
              int androidLayoutWidth = mLayoutWidget.getWidth() + widthPadding;
              int androidLayoutHeight = mLayoutWidget.getHeight() + heightPadding;
              if (mWidthMeasureMode == MeasureSpec.AT_MOST
                      || mWidthMeasureMode == MeasureSpec.UNSPECIFIED) {
                  androidLayoutWidth = (int) (mStartWrapWidth + mPostInterpolationPosition
                          * (mEndWrapWidth - mStartWrapWidth));
                  requestLayout();
              }
              if (mHeightMeasureMode == MeasureSpec.AT_MOST
                      || mHeightMeasureMode == MeasureSpec.UNSPECIFIED) {
                  androidLayoutHeight = (int) (mStartWrapHeight + mPostInterpolationPosition
                          * (mEndWrapHeight - mStartWrapHeight));
                  requestLayout();
              }
              setMeasuredDimension(androidLayoutWidth, androidLayoutHeight);
          }
          evaluateLayout();
      }
  
      @Override
      public boolean onStartNestedScroll(@NonNull View child,
                                         @NonNull View target,
                                         int axes, int type) {
          if (DEBUG) {
              Log.v(TAG, "********** onStartNestedScroll( child:" + Debug.getName(child)
                      + ", target:" + Debug.getName(target) + ", axis:" + axes + ", type:" + type);
          }
          if (mScene == null
                  || mScene.mCurrentTransition == null
                  || mScene.mCurrentTransition.getTouchResponse() == null
                  || (mScene.mCurrentTransition.getTouchResponse().getFlags()
                  & TouchResponse.FLAG_DISABLE_SCROLL) != 0) {
              return false;
          }
          return true;
      }
  
      @Override
      public void onNestedScrollAccepted(@NonNull View child,
                                         @NonNull View target,
                                         int axes,
                                         int type) {
          if (DEBUG) {
              Log.v(TAG, "********** onNestedScrollAccepted( child:" + Debug.getName(child)
                      + ", target:" + Debug.getName(target) + ", axis:" + axes + ", type:" + type);
          }
          mScrollTargetTime = getNanoTime();
          mScrollTargetDT = 0;
          mScrollTargetDX = 0;
          mScrollTargetDY = 0;
      }
  
      @Override
      public void onStopNestedScroll(@NonNull View target, int type) {
          if (DEBUG) {
              Log.v(TAG, "********** onStopNestedScroll(   target:"
                      + Debug.getName(target) + " , type:" + type + " "
                      + mScrollTargetDX + ", " + mScrollTargetDY);
              Debug.logStack(TAG, "onStopNestedScroll ", 8);
  
          }
          if (mScene == null || mScrollTargetDT == 0) {
              return;
          }
          mScene.processScrollUp(mScrollTargetDX / mScrollTargetDT,
                  mScrollTargetDY / mScrollTargetDT);
      }
  
      @Override
      public void onNestedScroll(@NonNull View target,
                                 int dxConsumed,
                                 int dyConsumed,
                                 int dxUnconsumed,
                                 int dyUnconsumed,
                                 int type, int[] consumed) {
          if (mUndergoingMotion || dxConsumed != 0 || dyConsumed != 0) {
              consumed[0] += dxUnconsumed;
              consumed[1] += dyUnconsumed;
          }
          mUndergoingMotion = false;
      }
  
      @Override
      public void onNestedScroll(@NonNull View target,
                                 int dxConsumed,
                                 int dyConsumed,
                                 int dxUnconsumed,
                                 int dyUnconsumed,
                                 int type) {
          if (DEBUG) {
              Log.v(TAG, "********** onNestedScroll( target:" + Debug.getName(target)
                      + ", dxConsumed:" + dxConsumed
                      + ", dyConsumed:" + dyConsumed
                      + ", dyConsumed:" + dxUnconsumed
                      + ", dyConsumed:" + dyUnconsumed + ", type:" + type);
          }
      }
  
      @Override
      public void onNestedPreScroll(@NonNull View target,
                                    int dx,
                                    int dy,
                                    @NonNull int[] consumed,
                                    int type) {
  
          MotionScene scene = mScene;
          if (scene == null) {
              return;
          }
  
          MotionScene.Transition currentTransition = scene.mCurrentTransition;
          if (currentTransition == null || !currentTransition.isEnabled()) {
              return;
          }
  
          if (currentTransition.isEnabled()) {
              TouchResponse touchResponse = currentTransition.getTouchResponse();
              if (touchResponse != null) {
                  int regionId = touchResponse.getTouchRegionId();
                  if (regionId != MotionScene.UNSET && target.getId() != regionId) {
                      return;
                  }
              }
          }
  
          if (scene.getMoveWhenScrollAtTop()) {
              // This blocks transition during scrolling
              TouchResponse touchResponse = currentTransition.getTouchResponse();
              int vert = -1;
              if (touchResponse != null) {
                  if ((touchResponse.getFlags() & TouchResponse.FLAG_SUPPORT_SCROLL_UP) != 0) {
                      vert = dy;
                  }
              }
              if ((mTransitionPosition == 1 || mTransitionPosition == 0)
                      && target.canScrollVertically(vert)) {
                  return;
              }
          }
  
          // This should be disabled in androidx
          if (currentTransition.getTouchResponse() != null
                  && (currentTransition.getTouchResponse().getFlags()
                  & TouchResponse.FLAG_DISABLE_POST_SCROLL) != 0) {
              float dir = scene.getProgressDirection(dx, dy);
              if ((mTransitionLastPosition <= 0.0f && (dir < 0))
                      || (mTransitionLastPosition >= 1.0f && (dir > 0))) {
                  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                      target.setNestedScrollingEnabled(false);
                      // TODO find a better hack
                      target.post(new Runnable() {
                          @Override
                          public void run() {
                              target.setNestedScrollingEnabled(true);
                          }
                      });
                  }
                  return;
              }
          }
  
          if (DEBUG) {
              Log.v(TAG, "********** onNestedPreScroll(target:"
                      + Debug.getName(target) + ", dx:" + dx + ", dy:" + dy + ", type:" + type);
          }
          float progress = mTransitionPosition;
          long time = getNanoTime();
          mScrollTargetDX = dx;
          mScrollTargetDY = dy;
          mScrollTargetDT = (float) ((time - mScrollTargetTime) * 1E-9);
          mScrollTargetTime = time;
          if (DEBUG) {
              Log.v(TAG, "********** dy = " + dx + " dy = " + dy + " dt = " + mScrollTargetDT);
          }
          scene.processScrollMove(dx, dy);
          if (progress != mTransitionPosition) {
              consumed[0] = dx;
              consumed[1] = dy;
          }
          evaluate(false);
          if (consumed[0] != 0 || consumed[1] != 0) {
              mUndergoingMotion = true;
          }
  
      }
  
      @Override
      public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
          return false;
      }
  
      @Override
      public boolean onNestedFling(@NonNull View target,
                                   float velocityX,
                                   float velocityY,
                                   boolean consumed) {
          return false;
      }
  
      ////////////////////////////////////////////////////////////////////////////////////////
      // Used to draw debugging lines
      ////////////////////////////////////////////////////////////////////////////////////////
      private class DevModeDraw {
          private static final int DEBUG_PATH_TICKS_PER_MS = 16;
          float[] mPoints;
          int[] mPathMode;
          float[] mKeyFramePoints;
          Path mPath;
          Paint mPaint;
          Paint mPaintKeyframes;
          Paint mPaintGraph;
          Paint mTextPaint;
          Paint mFillPaint;
          private float[] mRectangle;
          final int mRedColor = 0xFFFFAA33;
          final int mKeyframeColor = 0xffe0759a;
          final int mGraphColor = 0xFF33AA00;
          final int mShadowColor = 0x77000000;
          final int mDiamondSize = 10;
          DashPathEffect mDashPathEffect;
          int mKeyFrameCount;
          Rect mBounds = new Rect();
          boolean mPresentationMode = false;
          int mShadowTranslate = 1;
  
          DevModeDraw() {
              mPaint = new Paint();
              mPaint.setAntiAlias(true);
              mPaint.setColor(mRedColor);
              mPaint.setStrokeWidth(2);
              mPaint.setStyle(Paint.Style.STROKE);
  
              mPaintKeyframes = new Paint();
              mPaintKeyframes.setAntiAlias(true);
              mPaintKeyframes.setColor(mKeyframeColor);
              mPaintKeyframes.setStrokeWidth(2);
              mPaintKeyframes.setStyle(Paint.Style.STROKE);
  
              mPaintGraph = new Paint();
              mPaintGraph.setAntiAlias(true);
              mPaintGraph.setColor(mGraphColor);
              mPaintGraph.setStrokeWidth(2);
              mPaintGraph.setStyle(Paint.Style.STROKE);
  
              mTextPaint = new Paint();
              mTextPaint.setAntiAlias(true);
              mTextPaint.setColor(mGraphColor);
              mTextPaint.setTextSize(12 * getContext().getResources().getDisplayMetrics().density);
              mRectangle = new float[8];
              mFillPaint = new Paint();
              mFillPaint.setAntiAlias(true);
              mDashPathEffect = new DashPathEffect(new float[]{4, 8}, 0);
              mPaintGraph.setPathEffect(mDashPathEffect);
              mKeyFramePoints = new float[MAX_KEY_FRAMES * 2];
              mPathMode = new int[MAX_KEY_FRAMES];
  
              if (mPresentationMode) {
                  mPaint.setStrokeWidth(8);
                  mFillPaint.setStrokeWidth(8);
                  mPaintKeyframes.setStrokeWidth(8);
                  mShadowTranslate = 4;
              }
          }
  
          public void draw(Canvas canvas,
                           HashMap<View, MotionController> frameArrayList,
                           int duration, int debugPath) {
              if (frameArrayList == null || frameArrayList.size() == 0) {
                  return;
              }
              canvas.save();
              if (!isInEditMode() && (DEBUG_SHOW_PROGRESS & debugPath) == DEBUG_SHOW_PATH) {
                  String str = getContext().getResources().getResourceName(mEndState)
                          + ":" + getProgress();
                  canvas.drawText(str, 10, getHeight() - 30, mTextPaint);
                  canvas.drawText(str, 11, getHeight() - 29, mPaint);
              }
              for (MotionController motionController : frameArrayList.values()) {
                  int mode = motionController.getDrawPath();
                  if (debugPath > 0 && mode == MotionController.DRAW_PATH_NONE) {
                      mode = MotionController.DRAW_PATH_BASIC;
                  }
                  if (mode == MotionController.DRAW_PATH_NONE) { // do not draw path
                      continue;
                  }
  
                  mKeyFrameCount = motionController.buildKeyFrames(mKeyFramePoints, mPathMode);
  
                  if (mode >= MotionController.DRAW_PATH_BASIC) {
  
                      int frames = duration / DEBUG_PATH_TICKS_PER_MS;
                      if (mPoints == null || mPoints.length != frames * 2) {
                          mPoints = new float[frames * 2];
                          mPath = new Path();
                      }
  
                      canvas.translate(mShadowTranslate, mShadowTranslate);
  
                      mPaint.setColor(mShadowColor);
                      mFillPaint.setColor(mShadowColor);
                      mPaintKeyframes.setColor(mShadowColor);
                      mPaintGraph.setColor(mShadowColor);
                      motionController.buildPath(mPoints, frames);
                      drawAll(canvas, mode, mKeyFrameCount, motionController);
                      mPaint.setColor(mRedColor);
                      mPaintKeyframes.setColor(mKeyframeColor);
                      mFillPaint.setColor(mKeyframeColor);
                      mPaintGraph.setColor(mGraphColor);
  
                      canvas.translate(-mShadowTranslate, -mShadowTranslate);
                      drawAll(canvas, mode, mKeyFrameCount, motionController);
                      if (mode == MotionController.DRAW_PATH_RECTANGLE) {
                          drawRectangle(canvas, motionController);
                      }
                  }
  
              }
              canvas.restore();
          }
  
          public void drawAll(Canvas canvas,
                              int mode,
                              int keyFrames,
                              MotionController motionController) {
              if (mode == MotionController.DRAW_PATH_AS_CONFIGURED) {
                  drawPathAsConfigured(canvas);
              }
              if (mode == MotionController.DRAW_PATH_RELATIVE) {
                  drawPathRelative(canvas);
              }
              if (mode == MotionController.DRAW_PATH_CARTESIAN) {
                  drawPathCartesian(canvas);
              }
              drawBasicPath(canvas);
              drawTicks(canvas, mode, keyFrames, motionController);
          }
  
          private void drawBasicPath(Canvas canvas) {
              canvas.drawLines(mPoints, mPaint);
          }
  
          private void drawTicks(Canvas canvas,
                                 int mode,
                                 int keyFrames,
                                 MotionController motionController) {
              int viewWidth = 0;
              int viewHeight = 0;
              if (motionController.mView != null) {
                  viewWidth = motionController.mView.getWidth();
                  viewHeight = motionController.mView.getHeight();
              }
              for (int i = 1; i < keyFrames - 1; i++) {
                  if (mode == MotionController.DRAW_PATH_AS_CONFIGURED
                          && mPathMode[i - 1] == MotionController.DRAW_PATH_NONE) {
                      continue;
  
                  }
                  float x = mKeyFramePoints[i * 2];
                  float y = mKeyFramePoints[i * 2 + 1];
                  mPath.reset();
                  mPath.moveTo(x, y + mDiamondSize);
                  mPath.lineTo(x + mDiamondSize, y);
                  mPath.lineTo(x, y - mDiamondSize);
                  mPath.lineTo(x - mDiamondSize, y);
                  mPath.close();
  
                  @SuppressWarnings("unused")
                  MotionPaths framePoint = motionController.getKeyFrame(i - 1);
                  float dx = 0; //framePoint.translationX;
                  float dy = 0; //framePoint.translationY;
                  if (mode == MotionController.DRAW_PATH_AS_CONFIGURED) {
  
                      if (mPathMode[i - 1] == MotionPaths.PERPENDICULAR) {
                          drawPathRelativeTicks(canvas, x - dx, y - dy);
                      } else if (mPathMode[i - 1] == MotionPaths.CARTESIAN) {
                          drawPathCartesianTicks(canvas, x - dx, y - dy);
                      } else if (mPathMode[i - 1] == MotionPaths.SCREEN) {
                          drawPathScreenTicks(canvas, x - dx, y - dy, viewWidth, viewHeight);
                      }
  
                      canvas.drawPath(mPath, mFillPaint);
                  }
                  if (mode == MotionController.DRAW_PATH_RELATIVE) {
                      drawPathRelativeTicks(canvas, x - dx, y - dy);
                  }
                  if (mode == MotionController.DRAW_PATH_CARTESIAN) {
                      drawPathCartesianTicks(canvas, x - dx, y - dy);
                  }
                  if (mode == MotionController.DRAW_PATH_SCREEN) {
                      drawPathScreenTicks(canvas, x - dx, y - dy, viewWidth, viewHeight);
                  }
                  if (dx != 0 || dy != 0) {
                      drawTranslation(canvas, x - dx, y - dy, x, y);
                  } else {
                      canvas.drawPath(mPath, mFillPaint);
                  }
              }
              if (mPoints.length > 1) {
                  // Draw the starting and ending circle
                  canvas.drawCircle(mPoints[0], mPoints[1], 8, mPaintKeyframes);
                  canvas.drawCircle(mPoints[mPoints.length - 2],
                          mPoints[mPoints.length - 1], 8, mPaintKeyframes);
              }
          }
  
          private void drawTranslation(Canvas canvas, float x1, float y1, float x2, float y2) {
              canvas.drawRect(x1, y1, x2, y2, mPaintGraph);
              canvas.drawLine(x1, y1, x2, y2, mPaintGraph);
          }
  
          private void drawPathRelative(Canvas canvas) {
              canvas.drawLine(mPoints[0], mPoints[1],
                      mPoints[mPoints.length - 2], mPoints[mPoints.length - 1], mPaintGraph);
          }
  
          private void drawPathAsConfigured(Canvas canvas) {
              boolean path = false;
              boolean cart = false;
              for (int i = 0; i < mKeyFrameCount; i++) {
                  if (mPathMode[i] == MotionPaths.PERPENDICULAR) {
                      path = true;
                  }
                  if (mPathMode[i] == MotionPaths.CARTESIAN) {
                      cart = true;
                  }
              }
              if (path) {
                  drawPathRelative(canvas);
              }
              if (cart) {
                  drawPathCartesian(canvas);
              }
          }
  
          private void drawPathRelativeTicks(Canvas canvas, float x, float y) {
              float x1 = mPoints[0];
              float y1 = mPoints[1];
              float x2 = mPoints[mPoints.length - 2];
              float y2 = mPoints[mPoints.length - 1];
              float dist = (float) Math.hypot(x1 - x2, y1 - y2);
              float t = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / (dist * dist);
              float xp = x1 + t * (x2 - x1);
              float yp = y1 + t * (y2 - y1);
  
              Path path = new Path();
              path.moveTo(x, y);
              path.lineTo(xp, yp);
              float len = (float) Math.hypot(xp - x, yp - y);
              String text = "" + ((int) (100 * len / dist)) / 100.0f;
              getTextBounds(text, mTextPaint);
              float off = len / 2 - mBounds.width() / 2;
              canvas.drawTextOnPath(text, path, off, -20, mTextPaint);
              canvas.drawLine(x, y, xp, yp, mPaintGraph);
          }
  
          void getTextBounds(String text, Paint paint) {
              paint.getTextBounds(text, 0, text.length(), mBounds);
          }
  
          private void drawPathCartesian(Canvas canvas) {
              float x1 = mPoints[0];
              float y1 = mPoints[1];
              float x2 = mPoints[mPoints.length - 2];
              float y2 = mPoints[mPoints.length - 1];
  
              canvas.drawLine(Math.min(x1, x2), Math.max(y1, y2),
                      Math.max(x1, x2), Math.max(y1, y2), mPaintGraph);
              canvas.drawLine(Math.min(x1, x2), Math.min(y1, y2),
                      Math.min(x1, x2), Math.max(y1, y2), mPaintGraph);
          }
  
          private void drawPathCartesianTicks(Canvas canvas, float x, float y) {
              float x1 = mPoints[0];
              float y1 = mPoints[1];
              float x2 = mPoints[mPoints.length - 2];
              float y2 = mPoints[mPoints.length - 1];
              float minx = Math.min(x1, x2);
              float maxy = Math.max(y1, y2);
              float xgap = x - Math.min(x1, x2);
              float ygap = Math.max(y1, y2) - y;
              // Horizontal line
              String text = "" + ((int) (0.5 + 100 * xgap / Math.abs(x2 - x1))) / 100.0f;
              getTextBounds(text, mTextPaint);
              float off = xgap / 2 - mBounds.width() / 2;
              canvas.drawText(text, off + minx, y - 20, mTextPaint);
              canvas.drawLine(x, y,
                      Math.min(x1, x2), y, mPaintGraph);
  
              // Vertical line
              text = "" + ((int) (0.5 + 100 * ygap / Math.abs(y2 - y1))) / 100.0f;
              getTextBounds(text, mTextPaint);
              off = ygap / 2 - mBounds.height() / 2;
              canvas.drawText(text, x + 5, maxy - off, mTextPaint);
              canvas.drawLine(x, y,
                      x, Math.max(y1, y2), mPaintGraph);
          }
  
          private void drawPathScreenTicks(Canvas canvas,
                                           float x,
                                           float y,
                                           int viewWidth,
                                           int viewHeight) {
              float x1 = 0;
              float y1 = 0;
              float x2 = 1;
              float y2 = 1;
              float minx = 0;
              float maxy = 0;
              float xgap = x;
              float ygap = y;
              // Horizontal line
              String text = "" + ((int) (0.5 + 100 * (xgap - viewWidth / 2)
                      / (getWidth() - viewWidth))) / 100.0f;
              getTextBounds(text, mTextPaint);
              float off = xgap / 2 - mBounds.width() / 2;
              canvas.drawText(text, off + minx, y - 20, mTextPaint);
              canvas.drawLine(x, y,
                      Math.min(x1, x2), y, mPaintGraph);
  
              // Vertical line
              text = "" + ((int) (0.5 + 100 * (ygap - viewHeight / 2)
                      / (getHeight() - viewHeight))) / 100.0f;
              getTextBounds(text, mTextPaint);
              off = ygap / 2 - mBounds.height() / 2;
              canvas.drawText(text, x + 5, maxy - off, mTextPaint);
              canvas.drawLine(x, y,
                      x, Math.max(y1, y2), mPaintGraph);
          }
  
          private void drawRectangle(Canvas canvas, MotionController motionController) {
              mPath.reset();
              int rectFrames = 50;
              for (int i = 0; i <= rectFrames; i++) {
                  float p = i / (float) rectFrames;
                  motionController.buildRect(p, mRectangle, 0);
                  mPath.moveTo(mRectangle[0], mRectangle[1]);
                  mPath.lineTo(mRectangle[2], mRectangle[3]);
                  mPath.lineTo(mRectangle[4], mRectangle[5]);
                  mPath.lineTo(mRectangle[6], mRectangle[7]);
                  mPath.close();
              }
              mPaint.setColor(0x44000000);
              canvas.translate(2, 2);
              canvas.drawPath(mPath, mPaint);
  
              canvas.translate(-2, -2);
              mPaint.setColor(0xFFFF0000);
              canvas.drawPath(mPath, mPaint);
          }
  
      }
  
      @SuppressLint("LogConditional")
      private void debugPos() {
          for (int i = 0; i < getChildCount(); i++) {
              final View child = getChildAt(i);
              Log.v(TAG, " " + Debug.getLocation() + " " + Debug.getName(this)
                      + " " + Debug.getName(getContext(), mCurrentState) + " " + Debug.getName(child)
                      + child.getLeft() + " "
                      + child.getTop());
          }
      }
  
      /**
       * Used to draw debugging graphics and to do post layout changes
       *
       * @param canvas
       */
      @Override
      protected void dispatchDraw(Canvas canvas) {
          if (DEBUG) {
              Log.v(TAG, " dispatchDraw " + getProgress() + Debug.getLocation());
          }
          if (mDecoratorsHelpers != null) {
              for (MotionHelper decor : mDecoratorsHelpers) {
                  decor.onPreDraw(canvas);
              }
          }
          evaluate(false);
          if (mScene != null && mScene.mViewTransitionController != null) {
              mScene.mViewTransitionController.animate();
          }
          if (DEBUG) {
              Log.v(TAG, " dispatchDraw" + Debug.getLocation() + " " + Debug.getName(this)
                      + " " + Debug.getName(getContext(), mCurrentState));
              debugPos();
          }
          super.dispatchDraw(canvas);
          if (mScene == null) {
              return;
          }
          if (DEBUG) {
              mDebugPath = 0xFF;
          }
          if ((mDebugPath & 1) == 1) {
              if (!isInEditMode()) {
                  mFrames++;
                  long currentDrawTime = getNanoTime();
                  if (mLastDrawTime != -1) {
                      long delay = currentDrawTime - mLastDrawTime;
                      if (delay > 200000000) {
                          float fps = mFrames / (delay * 1E-9f);
                          mLastFps = ((int) (fps * 100)) / 100.0f;
                          mFrames = 0;
                          mLastDrawTime = currentDrawTime;
                      }
                  } else {
                      mLastDrawTime = currentDrawTime;
                  }
                  Paint paint = new Paint();
                  paint.setTextSize(42);
                  float p = ((int) (getProgress() * 1000)) / 10f;
                  String str = mLastFps + " fps " + Debug.getState(this, mBeginState) + " -> ";
                  str += Debug.getState(this, mEndState) + " (progress: " + p + " ) state="
                          + ((mCurrentState == UNSET) ? "undefined"
                                  : Debug.getState(this, mCurrentState));
                  paint.setColor(0xFF000000);
                  canvas.drawText(str, 11, getHeight() - 29, paint);
                  paint.setColor(0xFF880088);
                  canvas.drawText(str, 10, getHeight() - 30, paint);
  
              }
          }
          if (mDebugPath > 1) {
              if (mDevModeDraw == null) {
                  mDevModeDraw = new DevModeDraw();
              }
              mDevModeDraw.draw(canvas, mFrameArrayList, mScene.getDuration(), mDebugPath);
          }
          if (mDecoratorsHelpers != null) {
              for (MotionHelper decor : mDecoratorsHelpers) {
                  decor.onPostDraw(canvas);
              }
          }
      }
  
      /**
       * Direct layout evaluation
       */
      private void evaluateLayout() {
          float dir = Math.signum(mTransitionGoalPosition - mTransitionLastPosition);
          long currentTime = getNanoTime();
  
          float deltaPos = 0f;
          if (!(mInterpolator instanceof StopLogic)) { // if we are not in a drag
              deltaPos = dir * (currentTime - mTransitionLastTime) * 1E-9f / mTransitionDuration;
          }
          float position = mTransitionLastPosition + deltaPos;
  
          boolean done = false;
          if (mTransitionInstantly) {
              position = mTransitionGoalPosition;
          }
  
          if ((dir > 0 && position >= mTransitionGoalPosition)
                  || (dir <= 0 && position <= mTransitionGoalPosition)) {
              position = mTransitionGoalPosition;
              done = true;
          }
          if (mInterpolator != null && !done) {
              if (mTemporalInterpolator) {
                  float time = (currentTime - mAnimationStartTime) * 1E-9f;
                  position = mInterpolator.getInterpolation(time);
              } else {
                  position = mInterpolator.getInterpolation(position);
              }
          }
          if ((dir > 0 && position >= mTransitionGoalPosition)
                  || (dir <= 0 && position <= mTransitionGoalPosition)) {
              position = mTransitionGoalPosition;
          }
          mPostInterpolationPosition = position;
          int n = getChildCount();
          long time = getNanoTime();
          float interPos = mProgressInterpolator == null ? position
                  : mProgressInterpolator.getInterpolation(position);
          for (int i = 0; i < n; i++) {
              final View child = getChildAt(i);
              final MotionController frame = mFrameArrayList.get(child);
              if (frame != null) {
                  frame.interpolate(child, interPos, time, mKeyCache);
              }
          }
          if (mMeasureDuringTransition) {
              requestLayout();
          }
      }
  
      void endTrigger(boolean start) {
          int n = getChildCount();
          for (int i = 0; i < n; i++) {
              final View child = getChildAt(i);
              final MotionController frame = mFrameArrayList.get(child);
              if (frame != null) {
                  frame.endTrigger(start);
              }
          }
      }
  
      void evaluate(boolean force) {
  
          if (mTransitionLastTime == -1) {
              mTransitionLastTime = getNanoTime();
          }
          if (mTransitionLastPosition > 0.0f && mTransitionLastPosition < 1.0f) {
              mCurrentState = UNSET;
          }
  
          boolean newState = false;
          if (mKeepAnimating || (mInTransition
                  && (force || mTransitionGoalPosition != mTransitionLastPosition))) {
              float dir = Math.signum(mTransitionGoalPosition - mTransitionLastPosition);
              long currentTime = getNanoTime();
  
              float deltaPos = 0f;
              if (!(mInterpolator instanceof MotionInterpolator)) { // if we are not in a drag
                  deltaPos = dir * (currentTime - mTransitionLastTime) * 1E-9f / mTransitionDuration;
              }
              float position = mTransitionLastPosition + deltaPos;
  
              boolean done = false;
              if (mTransitionInstantly) {
                  position = mTransitionGoalPosition;
              }
  
              if ((dir > 0 && position >= mTransitionGoalPosition)
                      || (dir <= 0 && position <= mTransitionGoalPosition)) {
                  position = mTransitionGoalPosition;
                  mInTransition = false;
                  done = true;
              }
              if (DEBUG) {
                  Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = "
                          + mTransitionLastPosition + " position = " + position);
              }
              mTransitionLastPosition = position;
              mTransitionPosition = position;
              mTransitionLastTime = currentTime;
              int notStopLogic = 0;
              int stopLogicContinue = 1;
              int stopLogicStop = 2;
              int stopLogicDone = notStopLogic;
              if (mInterpolator != null && !done) {
                  if (mTemporalInterpolator) {
                      float time = (currentTime - mAnimationStartTime) * 1E-9f;
                      position = mInterpolator.getInterpolation(time);
                      if (mInterpolator == mStopLogic) {
                          boolean dp = mStopLogic.isStopped();
                          stopLogicDone = dp ? stopLogicStop : stopLogicContinue;
                      }
  
                      if (DEBUG) {
                          Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = "
                                  + mTransitionLastPosition + " position = " + position);
                      }
                      mTransitionLastPosition = position;
  
                      mTransitionLastTime = currentTime;
                      if (mInterpolator instanceof MotionInterpolator) {
                          float lastVelocity = ((MotionInterpolator) mInterpolator).getVelocity();
                          mLastVelocity = lastVelocity;
                          if (Math.abs(lastVelocity) * mTransitionDuration <= EPSILON
                                  && stopLogicDone == stopLogicStop) {
                              mInTransition = false;
                          }
                          if (lastVelocity > 0 && position >= 1.0f) {
                              mTransitionLastPosition = position = 1.0f;
                              mInTransition = false;
                          }
                          if (lastVelocity < 0 && position <= 0) {
                              mTransitionLastPosition = position = 0.0f;
                              mInTransition = false;
                          }
                      }
  
                  } else {
  
                      float p2 = position;
                      position = mInterpolator.getInterpolation(position);
                      if (mInterpolator instanceof MotionInterpolator) {
                          mLastVelocity = ((MotionInterpolator) mInterpolator).getVelocity();
                      } else {
                          p2 = mInterpolator.getInterpolation(p2 + deltaPos);
                          mLastVelocity = dir * (p2 - position) / deltaPos;
                      }
  
                  }
              } else {
                  mLastVelocity = deltaPos;
              }
              if (Math.abs(mLastVelocity) > EPSILON) {
                  setState(TransitionState.MOVING);
              }
  
              if (stopLogicDone != stopLogicContinue) {
                  if ((dir > 0 && position >= mTransitionGoalPosition)
                          || (dir <= 0 && position <= mTransitionGoalPosition)) {
                      position = mTransitionGoalPosition;
                      mInTransition = false;
                  }
  
                  if (position >= 1.0f || position <= 0.0f) {
                      mInTransition = false;
                      setState(TransitionState.FINISHED);
                  }
              }
  
              int n = getChildCount();
              mKeepAnimating = false;
              long time = getNanoTime();
              if (DEBUG) {
                  Log.v(TAG, "LAYOUT frame.interpolate at " + position);
              }
              mPostInterpolationPosition = position;
              float interPos = mProgressInterpolator == null ? position
                      : mProgressInterpolator.getInterpolation(position);
              if (mProgressInterpolator != null) {
                  mLastVelocity =
                          mProgressInterpolator
                                  .getInterpolation(position + dir / mTransitionDuration);
                  mLastVelocity -= mProgressInterpolator.getInterpolation(position);
              }
              for (int i = 0; i < n; i++) {
                  final View child = getChildAt(i);
                  final MotionController frame = mFrameArrayList.get(child);
                  if (frame != null) {
                      mKeepAnimating |= frame.interpolate(child, interPos, time, mKeyCache);
                  }
              }
              if (DEBUG) {
                  Log.v(TAG, " interpolate " + Debug.getLocation() + " " + Debug.getName(this)
                          + " " + Debug.getName(getContext(), mBeginState) + " " + position);
              }
  
              boolean end = ((dir > 0 && position >= mTransitionGoalPosition)
                      || (dir <= 0 && position <= mTransitionGoalPosition));
              if (!mKeepAnimating && !mInTransition && end) {
                  setState(TransitionState.FINISHED);
              }
              if (mMeasureDuringTransition) {
                  requestLayout();
              }
  
              mKeepAnimating |= !end;
  
              // If we have hit the begin state begin state could be unset
              if (position <= 0 && mBeginState != UNSET) {
                  if (mCurrentState != mBeginState) {
                      newState = true;
                      mCurrentState = mBeginState;
                      ConstraintSet set = mScene.getConstraintSet(mBeginState);
                      set.applyCustomAttributes(this);
                      setState(TransitionState.FINISHED);
                  }
              }
  
              if (position >= 1.0) {
                  if (DEBUG) {
                      Log.v(TAG, Debug.getLoc() + " ============= setting  to end "
                              + Debug.getName(getContext(), mEndState) + "  " + position);
                  }
                  if (mCurrentState != mEndState) {
                      newState = true;
                      mCurrentState = mEndState;
                      ConstraintSet set = mScene.getConstraintSet(mEndState);
                      set.applyCustomAttributes(this);
                      setState(TransitionState.FINISHED);
                  }
              }
  
              if (mKeepAnimating || mInTransition) {
                  invalidate();
              } else {
                  if ((dir > 0 && position == 1) || (dir < 0 && position == 0)) {
                      setState(TransitionState.FINISHED);
                  }
              }
              if (!mKeepAnimating && !mInTransition && ((dir > 0 && position == 1)
                      || (dir < 0 && position == 0))) {
                  onNewStateAttachHandlers();
              }
          }
          if (mTransitionLastPosition >= 1.0f) {
              if (mCurrentState != mEndState) {
                  newState = true;
              }
              mCurrentState = mEndState;
          } else if (mTransitionLastPosition <= 0.0f) {
              if (mCurrentState != mBeginState) {
                  newState = true;
              }
              mCurrentState = mBeginState;
          }
  
          mNeedsFireTransitionCompleted |= newState;
  
          if (newState && !mInLayout) {
              requestLayout();
          }
  
          mTransitionPosition = mTransitionLastPosition;
      }
  
      private boolean mNeedsFireTransitionCompleted = false;
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          mInLayout = true;
          try {
              if (DEBUG) {
                  Log.v(TAG, " onLayout " + getProgress() + "  " + Debug.getLocation());
              }
              if (mScene == null) {
                  super.onLayout(changed, left, top, right, bottom);
                  return;
              }
              int w = right - left;
              int h = bottom - top;
              if (mLastLayoutWidth != w || mLastLayoutHeight != h) {
                  rebuildScene();
                  evaluate(true);
                  if (DEBUG) {
                      Log.v(TAG, " onLayout  rebuildScene  " + Debug.getLocation());
                  }
              }
  
              mLastLayoutWidth = w;
              mLastLayoutHeight = h;
              mOldWidth = w;
              mOldHeight = h;
          } finally {
              mInLayout = false;
          }
      }
  
      /**
       * block ConstraintLayout from handling layout description
       *
       * @param id
       */
      @Override
      protected void parseLayoutDescription(int id) {
          mConstraintLayoutSpec = null;
      }
  
      private void init(AttributeSet attrs) {
          IS_IN_EDIT_MODE = isInEditMode();
          if (attrs != null) {
              TypedArray a = getContext()
                      .obtainStyledAttributes(attrs, R.styleable.MotionLayout);
              final int count = a.getIndexCount();
  
              boolean apply = true;
              for (int i = 0; i < count; i++) {
                  int attr = a.getIndex(i);
                  if (attr == R.styleable.MotionLayout_layoutDescription) {
                      int n = a.getResourceId(attr, UNSET);
                      mScene = new MotionScene(getContext(), this, n);
                  } else if (attr == R.styleable.MotionLayout_currentState) {
                      mCurrentState = a.getResourceId(attr, UNSET);
                  } else if (attr == R.styleable.MotionLayout_motionProgress) {
                      mTransitionGoalPosition = a.getFloat(attr, 0.0f);
                      mInTransition = true;
                  } else if (attr == R.styleable.MotionLayout_applyMotionScene) {
                      apply = a.getBoolean(attr, apply);
                  } else if (attr == R.styleable.MotionLayout_showPaths) {
                      if (mDebugPath == 0) { // favor motionDebug
                          mDebugPath = a.getBoolean(attr, false) ? DEBUG_SHOW_PATH : 0;
                      }
                  } else if (attr == R.styleable.MotionLayout_motionDebug) {
                      mDebugPath = a.getInt(attr, 0);
                  }
              }
              a.recycle();
              if (mScene == null) {
                  Log.e(TAG, "WARNING NO app:layoutDescription tag");
              }
              if (!apply) {
                  mScene = null;
              }
          }
          if (mDebugPath != 0) {
              checkStructure();
          }
          if (mCurrentState == UNSET && mScene != null) {
  
              mCurrentState = mScene.getStartId();
              mBeginState = mScene.getStartId();
              if (DEBUG) {
                  Log.v(TAG, " ============= init   end is "
                          + Debug.getName(getContext(), mEndState));
              }
              mEndState = mScene.getEndId();
              if (DEBUG) {
                  Log.v(TAG, " ============= init setting end to "
                          + Debug.getName(getContext(), mEndState));
              }
          }
      }
  
      /**
       * Sets a motion scene to the layout. Subsequent calls to it will override the previous scene.
       */
      public void setScene(MotionScene scene) {
          mScene = scene;
          mScene.setRtl(isRtl());
          rebuildScene();
      }
  
      /**
       * Get the motion scene of the layout.
       * Warning! This gives you direct access to the internal
       * state of the MotionLayout making it easy
       * corrupt the state.
       * @return the motion scene
       */
      public MotionScene getScene() {
          return mScene;
      }
  
      private void checkStructure() {
          if (mScene == null) {
              Log.e(TAG, "CHECK: motion scene not set! set \"app:layoutDescription=\"@xml/file\"");
              return;
          }
  
          checkStructure(mScene.getStartId(), mScene.getConstraintSet(mScene.getStartId()));
          SparseIntArray startToEnd = new SparseIntArray();
          SparseIntArray endToStart = new SparseIntArray();
          for (MotionScene.Transition definedTransition : mScene.getDefinedTransitions()) {
              if (definedTransition == mScene.mCurrentTransition) {
                  Log.v(TAG, "CHECK: CURRENT");
              }
              checkStructure(definedTransition);
              int startId = definedTransition.getStartConstraintSetId();
              int endId = definedTransition.getEndConstraintSetId();
              String startString = Debug.getName(getContext(), startId);
              String endString = Debug.getName(getContext(), endId);
              if (startToEnd.get(startId) == endId) {
  
                  Log.e(TAG, "CHECK: two transitions with the same start and end "
                          + startString + "->" + endString);
              }
              if (endToStart.get(endId) == startId) {
  
                  Log.e(TAG, "CHECK: you can't have reverse transitions"
                          + startString + "->" + endString);
              }
              startToEnd.put(startId, endId);
              endToStart.put(endId, startId);
              if (mScene.getConstraintSet(startId) == null) {
                  Log.e(TAG, " no such constraintSetStart " + startString);
              }
  
              if (mScene.getConstraintSet(endId) == null) {
                  Log.e(TAG, " no such constraintSetEnd " + startString);
              }
          }
      }
  
      private void checkStructure(int csetId, ConstraintSet set) {
          String setName = Debug.getName(getContext(), csetId);
          int size = getChildCount();
          for (int i = 0; i < size; i++) {
              View v = getChildAt(i);
              int id = v.getId();
              if (id == -1) {
                  Log.w(TAG, "CHECK: " + setName + " ALL VIEWS SHOULD HAVE ID's "
                          + v.getClass().getName() + " does not!");
              }
              ConstraintSet.Constraint c = set.getConstraint(id);
              if (c == null) {
                  Log.w(TAG, "CHECK: " + setName + " NO CONSTRAINTS for " + Debug.getName(v));
              }
          }
          int[] ids = set.getKnownIds();
          for (int i = 0; i < ids.length; i++) {
              int id = ids[i];
              String idString = Debug.getName(getContext(), id);
              if (null == findViewById(ids[i])) {
                  Log.w(TAG, "CHECK: " + setName + " NO View matches id " + idString);
              }
              if (set.getHeight(id) == UNSET) {
                  Log.w(TAG, "CHECK: " + setName + "(" + idString + ") no LAYOUT_HEIGHT");
              }
              if (set.getWidth(id) == UNSET) {
                  Log.w(TAG, "CHECK: " + setName + "(" + idString + ") no LAYOUT_HEIGHT");
              }
          }
      }
  
      private void checkStructure(MotionScene.Transition transition) {
          if (DEBUG) {
              Log.v(TAG, "CHECK: transition = " + transition.debugString(getContext()));
              Log.v(TAG, "CHECK: transition.setDuration = " + transition.getDuration());
          }
          if (transition.getStartConstraintSetId() == transition.getEndConstraintSetId()) {
              Log.e(TAG, "CHECK: start and end constraint set should not be the same!");
          }
      }
  
      /**
       * Display the debugging information such as paths information
       *
       * @param debugMode integer representing various debug modes
       *
       */
      public void setDebugMode(int debugMode) {
          mDebugPath = debugMode;
          invalidate();
      }
  
      private RectF mBoundsCheck = new RectF();
      private View mRegionView = null;
      private Matrix mInverseMatrix = null;
  
      private boolean callTransformedTouchEvent(View view,
                                                MotionEvent event,
                                                float offsetX,
                                                float offsetY) {
          Matrix viewMatrix = view.getMatrix();
  
          if (viewMatrix.isIdentity()) {
              event.offsetLocation(offsetX, offsetY);
              boolean handled = view.onTouchEvent(event);
              event.offsetLocation(-offsetX, -offsetY);
  
              return handled;
          }
  
          MotionEvent transformedEvent = MotionEvent.obtain(event);
  
          transformedEvent.offsetLocation(offsetX, offsetY);
  
          if (mInverseMatrix == null) {
              mInverseMatrix = new Matrix();
          }
  
          viewMatrix.invert(mInverseMatrix);
          transformedEvent.transform(mInverseMatrix);
  
          boolean handled = view.onTouchEvent(transformedEvent);
  
          transformedEvent.recycle();
  
          return handled;
      }
  
      /**
       * Walk the view tree to see if a child view handles a touch event.
       *
       * @param x
       * @param y
       * @param view
       * @param event
       * @return
       */
      private boolean handlesTouchEvent(float x, float y, View view, MotionEvent event) {
          boolean handled = false;
          if (view instanceof ViewGroup) {
              ViewGroup group = (ViewGroup) view;
              final int childCount = group.getChildCount();
              for (int i = childCount - 1; i >= 0; i--) {
                  View child = group.getChildAt(i);
                  if (handlesTouchEvent(x + child.getLeft() - view.getScrollX(),
                          y + child.getTop() - view.getScrollY(),
                          child, event)) {
                      handled = true;
                      break;
                  }
              }
          }
  
          if (!handled) {
              mBoundsCheck.set(x, y,
                      x + view.getRight() - view.getLeft(),
                      y + view.getBottom() - view.getTop());
  
              if (event.getAction() != MotionEvent.ACTION_DOWN
                      || mBoundsCheck.contains(event.getX(), event.getY())) {
                  if (callTransformedTouchEvent(view, event, -x, -y)) {
                      handled = true;
                  }
              }
          }
  
          return handled;
      }
  
      /**
       * Intercepts the touch event to correctly handle touch region id handover
       *
       * @param event
       * @return
       */
      @Override
      public boolean onInterceptTouchEvent(MotionEvent event) {
          if (mScene == null || !mInteractionEnabled) {
              return false;
          }
  
          if (mScene.mViewTransitionController != null) {
              mScene.mViewTransitionController.touchEvent(event);
          }
          MotionScene.Transition currentTransition = mScene.mCurrentTransition;
          if (currentTransition != null && currentTransition.isEnabled()) {
              TouchResponse touchResponse = currentTransition.getTouchResponse();
              if (touchResponse != null) {
                  if (event.getAction() == MotionEvent.ACTION_DOWN) {
                      RectF region = touchResponse.getTouchRegion(this, new RectF());
                      if (region != null
                              && !region.contains(event.getX(), event.getY())) {
                          return false;
                      }
                  }
                  int regionId = touchResponse.getTouchRegionId();
                  if (regionId != MotionScene.UNSET) {
                      if (mRegionView == null || mRegionView.getId() != regionId) {
                          mRegionView = findViewById(regionId);
                      }
                      if (mRegionView != null) {
                          mBoundsCheck.set(mRegionView.getLeft(),
                                  mRegionView.getTop(),
                                  mRegionView.getRight(),
                                  mRegionView.getBottom());
                          if (mBoundsCheck.contains(event.getX(), event.getY())) {
                              // In case of region id, if the view or a child of the view
                              // handles an event we don't need to do anything;
                              if (!handlesTouchEvent(mRegionView.getLeft(), mRegionView.getTop(),
                                      mRegionView, event)) {
                                  // but if not, then *we* need to handle the event.
                                  return onTouchEvent(event);
                              }
                          }
                      }
                  }
              }
          }
          return false;
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          if (DEBUG) {
              Log.v(TAG, Debug.getLocation() + " onTouchEvent = " + mTransitionLastPosition);
          }
          if (mScene != null && mInteractionEnabled && mScene.supportTouch()) {
              MotionScene.Transition currentTransition = mScene.mCurrentTransition;
              if (currentTransition != null && !currentTransition.isEnabled()) {
                  return super.onTouchEvent(event);
              }
              mScene.processTouchEvent(event, getCurrentState(), this);
              if (mScene.mCurrentTransition.isTransitionFlag(TRANSITION_FLAG_INTERCEPT_TOUCH)) {
                  return mScene.mCurrentTransition.getTouchResponse().isDragStarted();
              }
              return true;
          }
          if (DEBUG) {
              Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = "
                      + mTransitionLastPosition);
          }
          return super.onTouchEvent(event);
      }
  
      @Override
      protected void onAttachedToWindow() {
          super.onAttachedToWindow();
          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;
              @SuppressWarnings("unused")
              float dydp = (deltaPos != 0.0f) ? deltaY / deltaPos : Float.NaN;
              if (DEBUG) {
                  Log.v(TAG, " getAnchorDpDt " + Debug.getName(v) + " "
                          + Debug.getLocation() + " " + Arrays.toString(mAnchorDpDt));
              }
  
              mLastPos = pos;
              mLastY = y;
          } else {
              String idName = (v == null) ? "" + mTouchAnchorId :
                      v.getContext().getResources().getResourceName(mTouchAnchorId);
              Log.w(TAG, "WARNING could not find view id " + idName);
          }
      }
  
      /**
       * Gets the time of the currently set animation.
       *
       * @return time in Milliseconds
       */
      public long getTransitionTimeMs() {
          if (mScene != null) {
              mTransitionDuration = mScene.getDuration() / 1000f;
          }
          return (long) (mTransitionDuration * 1000);
      }
  
      /**
       * Set a listener to be notified of drawer events.
       *
       * @param listener Listener to notify when drawer events occur
       * @see TransitionListener
       */
      public void setTransitionListener(TransitionListener listener) {
          mTransitionListener = listener;
      }
  
      /**
       * adds a listener to be notified of drawer events.
       *
       * @param listener Listener to notify when drawer events occur
       * @see TransitionListener
       */
      public void addTransitionListener(TransitionListener listener) {
          if (mTransitionListeners == null) {
              mTransitionListeners = new CopyOnWriteArrayList<>();
          }
          mTransitionListeners.add(listener);
      }
  
      /**
       * adds a listener to be notified of drawer events.
       *
       * @param listener Listener to notify when drawer events occur
       * @return <tt>true</tt> if it contained the specified listener
       * @see TransitionListener
       */
      public boolean removeTransitionListener(TransitionListener listener) {
          if (mTransitionListeners == null) {
              return false;
          }
          return mTransitionListeners.remove(listener);
      }
  
      /**
       * Listener for monitoring events about TransitionLayout. <b>Added in 2.0</b>
       */
      public interface TransitionListener {
          /**
           * Called when a drawer is about to start a transition.
           * Note. startId may be -1 if starting from an "undefined state"
           *
           * @param motionLayout The TransitionLayout view that was moved
           * @param startId      the id of the start state (or ConstraintSet). Will be -1 if unknown.
           * @param endId        the id of the end state (or ConstraintSet).
           */
          void onTransitionStarted(MotionLayout motionLayout,
                                          int startId, int endId);
  
          /**
           * Called when a drawer's position changes.
           *
           * @param motionLayout The TransitionLayout view that was moved
           * @param startId      the id of the start state (or ConstraintSet). Will be -1 if unknown.
           * @param endId        the id of the end state (or ConstraintSet).
           * @param progress     The progress on this transition, from 0 to 1.
           */
          void onTransitionChange(MotionLayout motionLayout,
                                  int startId, int endId,
                                  float progress);
  
          /**
           * Called when a drawer has settled completely a state.
           * The TransitionLayout is interactive at this point.
           *
           * @param motionLayout Drawer view that is now open
           * @param currentId    the id it has reached
           */
          void onTransitionCompleted(MotionLayout motionLayout, int currentId);
  
          /**
           * Call when a trigger is fired
           *
           * @param motionLayout
           * @param triggerId    The id set set with triggerID
           * @param positive     for positive transition edge
           * @param progress
           */
          void onTransitionTrigger(MotionLayout motionLayout, int triggerId, boolean positive,
                                   float progress);
      }
  
      /**
       * This causes the callback onTransitionTrigger to be called
       *
       * @param triggerId The id set set with triggerID
       * @param positive  for positive transition edge
       * @param progress  the current progress
       */
      public void fireTrigger(int triggerId, boolean positive, float progress) {
          if (mTransitionListener != null) {
              mTransitionListener.onTransitionTrigger(this, triggerId, positive, progress);
          }
          if (mTransitionListeners != null) {
              for (TransitionListener listeners : mTransitionListeners) {
                  listeners.onTransitionTrigger(this, triggerId, positive, progress);
              }
          }
      }
  
      private void fireTransitionChange() {
          if (mTransitionListener != null
                  || (mTransitionListeners != null && !mTransitionListeners.isEmpty())) {
              if (mListenerPosition != mTransitionPosition) {
                  if (mListenerState != UNSET) {
                      fireTransitionStarted();
                      mIsAnimating = true;
                  }
                  mListenerState = UNSET;
                  mListenerPosition = mTransitionPosition;
                  if (mTransitionListener != null) {
                      mTransitionListener.onTransitionChange(this,
                              mBeginState, mEndState, mTransitionPosition);
                  }
                  if (mTransitionListeners != null) {
                      for (TransitionListener listeners : mTransitionListeners) {
                          listeners.onTransitionChange(this,
                                  mBeginState, mEndState, mTransitionPosition);
                      }
                  }
                  mIsAnimating = true;
              }
          }
      }
  
      ArrayList<Integer> mTransitionCompleted = new ArrayList<>();
  
      /**
       * This causes the callback TransitionCompleted to be called
       */
      protected void fireTransitionCompleted() {
          if (mTransitionListener != null
                  || (mTransitionListeners != null && !mTransitionListeners.isEmpty())) {
              if (mListenerState == UNSET) {
                  mListenerState = mCurrentState;
                  int lastState = UNSET;
                  if (!mTransitionCompleted.isEmpty()) {
                      lastState = mTransitionCompleted.get(mTransitionCompleted.size() - 1);
                  }
                  if (lastState != mCurrentState && mCurrentState != -1) {
                      mTransitionCompleted.add(mCurrentState);
                  }
              }
          }
          processTransitionCompleted();
          if (mOnComplete != null) {
              mOnComplete.run();
              mOnComplete = null;
          }
  
          if (mScheduledTransitionTo != null && mScheduledTransitions > 0) {
              transitionToState(mScheduledTransitionTo[0]);
              System.arraycopy(mScheduledTransitionTo,
                      1, mScheduledTransitionTo,
                      0, mScheduledTransitionTo.length - 1);
              mScheduledTransitions--;
          }
      }
  
      private void processTransitionCompleted() {
          if (mTransitionListener == null
                  && (mTransitionListeners == null || mTransitionListeners.isEmpty())) {
              return;
          }
          mIsAnimating = false;
          for (Integer state : mTransitionCompleted) {
              if (mTransitionListener != null) {
                  mTransitionListener.onTransitionCompleted(this, state);
              }
              if (mTransitionListeners != null) {
                  for (TransitionListener listeners : mTransitionListeners) {
                      listeners.onTransitionCompleted(this, state);
                  }
              }
          }
          mTransitionCompleted.clear();
      }
  
      /**
       *
       */
      public DesignTool getDesignTool() {
          if (mDesignTool == null) {
              mDesignTool = new DesignTool(this);
          }
          return mDesignTool;
      }
  
      /**
       *
       */
      @Override
      public void onViewAdded(View view) {
          super.onViewAdded(view);
          if (view instanceof MotionHelper) {
              MotionHelper helper = (MotionHelper) view;
              if (mTransitionListeners == null) {
                  mTransitionListeners = new CopyOnWriteArrayList<>();
              }
              mTransitionListeners.add(helper);
  
              if (helper.isUsedOnShow()) {
                  if (mOnShowHelpers == null) {
                      mOnShowHelpers = new ArrayList<>();
                  }
                  mOnShowHelpers.add(helper);
              }
              if (helper.isUseOnHide()) {
                  if (mOnHideHelpers == null) {
                      mOnHideHelpers = new ArrayList<>();
                  }
                  mOnHideHelpers.add(helper);
              }
              if (helper.isDecorator()) {
                  if (mDecoratorsHelpers == null) {
                      mDecoratorsHelpers = new ArrayList<>();
                  }
                  mDecoratorsHelpers.add(helper);
              }
          }
      }
  
      /**
       *
       */
      @Override
      public void onViewRemoved(View view) {
          super.onViewRemoved(view);
          if (mOnShowHelpers != null) {
              mOnShowHelpers.remove(view);
          }
          if (mOnHideHelpers != null) {
              mOnHideHelpers.remove(view);
          }
      }
  
      /**
       * Notify OnShow motion helpers
       * @param progress
       */
      public void setOnShow(float progress) {
          if (mOnShowHelpers != null) {
              final int count = mOnShowHelpers.size();
              for (int i = 0; i < count; i++) {
                  MotionHelper helper = mOnShowHelpers.get(i);
                  helper.setProgress(progress);
              }
          }
      }
  
      /**
       * Notify OnHide motion helpers
       * @param progress
       */
      public void setOnHide(float progress) {
          if (mOnHideHelpers != null) {
              final int count = mOnHideHelpers.size();
              for (int i = 0; i < count; i++) {
                  MotionHelper helper = mOnHideHelpers.get(i);
                  helper.setProgress(progress);
              }
          }
      }
  
      /**
       * Get the id's of all constraintSets used by MotionLayout
       *
       * @return
       */
      public  @IdRes
              int[] getConstraintSetIds() {
          if (mScene == null) {
              return null;
          }
          return mScene.getConstraintSetIds();
      }
  
      /**
       * Get the id's of all constraintSets with the matching types
       *
       * @return
       */
      public int[] getMatchingConstraintSetIds(String ... types) {
          if (mScene == null) {
              return null;
          }
          return mScene.getMatchingStateLabels(types);
      }
  
      /**
       * Get the ConstraintSet associated with an id
       * This returns a link to the constraintSet
       * But in most cases can be used.
       * createConstraintSet makes a copy which is more expensive.
       *
       * @param id of the constraintSet
       * @return ConstraintSet of MotionLayout
       * @see #cloneConstraintSet(int)
       */
      public ConstraintSet getConstraintSet(int id) {
          if (mScene == null) {
              return null;
          }
          return mScene.getConstraintSet(id);
      }
  
      /**
       * Creates a ConstraintSet based on an existing
       * constraintSet.
       * This makes a copy of the ConstraintSet.
       *
       * @param id The ide of the ConstraintSet
       * @return the ConstraintSet
       */
      public ConstraintSet cloneConstraintSet(int id) {
          if (mScene == null) {
              return null;
          }
          ConstraintSet orig = mScene.getConstraintSet(id);
          ConstraintSet ret = new ConstraintSet();
          ret.clone(orig);
          return ret;
      }
  
      /**
       * rebuild the motion Layouts
       *
       * @deprecated Please call rebuildScene() instead.
       */
      @Deprecated
      public void rebuildMotion() {
          Log.e(TAG, "This method is deprecated. Please call rebuildScene() instead.");
          rebuildScene();
      }
  
      /**
       * rebuild the motion Layouts
       */
      public void rebuildScene() {
          mModel.reEvaluateState();
          invalidate();
      }
  
      /**
       * update a ConstraintSet under the id.
       *
       * @param stateId id of the ConstraintSet
       * @param set     The constraintSet
       */
      public void updateState(int stateId, ConstraintSet set) {
          if (mScene != null) {
              mScene.setConstraintSet(stateId, set);
          }
          updateState();
          if (mCurrentState == stateId) {
              set.applyTo(this);
          }
      }
  
      /**
       * Update a ConstraintSet but animate the change.
       *
       * @param stateId  id of the ConstraintSet
       * @param set      The constraintSet
       * @param duration The length of time to perform the animation
       */
      public void updateStateAnimate(int stateId, ConstraintSet set, int duration) {
          if (mScene == null) {
              return;
          }
  
          if (mCurrentState == stateId) {
              updateState(R.id.view_transition, getConstraintSet(stateId));
              setState(R.id.view_transition, -1, -1);
              updateState(stateId, set);
              MotionScene.Transition tmpTransition =
                      new MotionScene.Transition(-1, mScene, R.id.view_transition, stateId);
              tmpTransition.setDuration(duration);
              setTransition(tmpTransition);
              transitionToEnd();
          }
      }
  
      /**
       * on completing the current transition, transition to this state.
       *
       * @param id
       */
      public void scheduleTransitionTo(int id) {
          if (getCurrentState() == -1) {
              transitionToState(id);
          } else {
              if (mScheduledTransitionTo == null) {
                  mScheduledTransitionTo = new int[4];
              } else if (mScheduledTransitionTo.length <= mScheduledTransitions) {
                  mScheduledTransitionTo =
                          Arrays.copyOf(mScheduledTransitionTo, mScheduledTransitionTo.length * 2);
              }
              mScheduledTransitionTo[mScheduledTransitions++] = id;
          }
      }
  
      /**
       * Not sure we want this
       *
       *
       */
      public void updateState() {
          mModel.initFrom(mLayoutWidget,
                  mScene.getConstraintSet(mBeginState),
                  mScene.getConstraintSet(mEndState));
          rebuildScene();
      }
  
      /**
       * Get all Transitions known to the system.
       *
       * @return
       */
      public ArrayList<MotionScene.Transition> getDefinedTransitions() {
          if (mScene == null) {
              return null;
          }
          return mScene.getDefinedTransitions();
      }
  
      /**
       * Gets the state you are currently transitioning from.
       * If you are transitioning from an unknown state returns -1
       *
       * @return State you are transitioning from.
       */
      public int getStartState() {
          return mBeginState;
      }
  
      /**
       * Gets the state you are currently transition to.
       *
       * @return The State you are transitioning to.
       */
      public int getEndState() {
          return mEndState;
      }
  
      /**
       * Gets the position you are animating to typically 0 or 1.
       * This is useful during animation after touch up
       *
       * @return The target position you are moving to
       */
      public float getTargetPosition() {
          return mTransitionGoalPosition;
      }
  
      /**
       * Change the current Transition duration.
       *
       * @param milliseconds duration for transition to complete
       */
      public void setTransitionDuration(int milliseconds) {
          if (mScene == null) {
              Log.e(TAG, "MotionScene not defined");
              return;
          }
          mScene.setDuration(milliseconds);
      }
  
      /**
       * This returns the internal Transition Structure
       *
       * @param id
       * @return
       */
      public MotionScene.Transition getTransition(int id) {
          return mScene.getTransitionById(id);
      }
  
      /**
       * This looks up the constraintset ID given an id string (
       *
       * @param id String id (without the "@+id/")
       * @return the integer id of the string
       *
       */
      int lookUpConstraintId(String id) {
          if (mScene == null) {
              return 0;
          }
          return mScene.lookUpConstraintId(id);
      }
  
      /**
       * does a revers look up to find the ConstraintSets Name
       *
       * @param id the integer id of the constraintSet
       * @return
       */
      String getConstraintSetNames(int id) {
          if (mScene == null) {
              return null;
          }
          return mScene.lookUpConstraintName(id);
      }
  
      /**
       * this allow disabling autoTransitions to prevent design surface from being in undefined states
       *
       * @param disable
       */
      void disableAutoTransition(boolean disable) {
          if (mScene == null) {
              return;
          }
          mScene.disableAutoTransition(disable);
      }
  
      /**
       * Enables (or disables) MotionLayout's onClick and onSwipe handling.
       *
       * @param enabled If true,  touch & click  is enabled; otherwise it is disabled
       */
      public void setInteractionEnabled(boolean enabled) {
          mInteractionEnabled = enabled;
      }
  
      /**
       * Determines whether MotionLayout's touch & click handling are enabled.
       * An interaction enabled MotionLayout can respond to user input and initiate and control.
       * MotionLayout interactions are enabled initially by default.
       * MotionLayout touch & click handling may be enabled or disabled by calling its
       * setInteractionEnabled method.
       *
       * @return true if MotionLayout's  touch & click  is enabled, false otherwise
       */
      public boolean isInteractionEnabled() {
          return mInteractionEnabled;
      }
  
      private void fireTransitionStarted() {
          if (mTransitionListener != null) {
              mTransitionListener.onTransitionStarted(this, mBeginState, mEndState);
          }
          if (mTransitionListeners != null) {
              for (TransitionListener listeners : mTransitionListeners) {
                  listeners.onTransitionStarted(this, mBeginState, mEndState);
              }
          }
      }
  
      /**
       * Execute a ViewTransition.
       * Transition will execute if its conditions are met and it is enabled
       *
       * @param viewTransitionId
       * @param view             The views to apply to
       */
      public void viewTransition(int viewTransitionId, View... view) {
          if (mScene != null) {
              mScene.viewTransition(viewTransitionId, view);
          } else {
              Log.e(TAG, " no motionScene");
          }
      }
  
      /**
       * Enable a ViewTransition ID.
       *
       * @param viewTransitionId id of ViewTransition
       * @param enable           If false view transition cannot be executed.
       */
      public void enableViewTransition(int viewTransitionId, boolean enable) {
          if (mScene != null) {
              mScene.enableViewTransition(viewTransitionId, enable);
          }
      }
  
      /**
       * Is transition id enabled or disabled
       *
       * @param viewTransitionId the ide of the transition
       * @return true if enabled
       */
      public boolean isViewTransitionEnabled(int viewTransitionId) {
          if (mScene != null) {
              return mScene.isViewTransitionEnabled(viewTransitionId);
          }
          return false;
      }
  
      /**
       * Apply the view transitions keyFrames to the MotionController.
       * Note ConstraintOverride is not used
       *
       * @param viewTransitionId the id of the view transition
       * @param motionController the MotionController to apply the keyframes to
       * @return true if it found and applied the viewTransition false otherwise
       */
      public boolean applyViewTransition(int viewTransitionId, MotionController motionController) {
          if (mScene != null) {
              return mScene.applyViewTransition(viewTransitionId, motionController);
          }
          return false;
      }
  
      /**
       * Is initial state changes are applied during onAttachedToWindow or after.
       * @return
       */
      public boolean isDelayedApplicationOfInitialState() {
          return mDelayedApply;
      }
  
      /**
       * Initial state changes are applied during onAttachedToWindow unless this is set to true.
       * @param delayedApply
       */
      public void setDelayedApplicationOfInitialState(boolean delayedApply) {
          this.mDelayedApply = delayedApply;
      }
  
  }
  