content: /*
   * Copyright (C) 2021 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.core.state;
  
  import androidx.annotation.NonNull;
  import androidx.constraintlayout.core.motion.CustomVariable;
  import androidx.constraintlayout.core.motion.Motion;
  import androidx.constraintlayout.core.motion.MotionWidget;
  import androidx.constraintlayout.core.motion.key.MotionKeyAttributes;
  import androidx.constraintlayout.core.motion.key.MotionKeyCycle;
  import androidx.constraintlayout.core.motion.key.MotionKeyPosition;
  import androidx.constraintlayout.core.motion.utils.Easing;
  import androidx.constraintlayout.core.motion.utils.KeyCache;
  import androidx.constraintlayout.core.motion.utils.SpringStopEngine;
  import androidx.constraintlayout.core.motion.utils.StopEngine;
  import androidx.constraintlayout.core.motion.utils.StopLogicEngine;
  import androidx.constraintlayout.core.motion.utils.TypedBundle;
  import androidx.constraintlayout.core.motion.utils.TypedValues;
  import androidx.constraintlayout.core.motion.utils.Utils;
  import androidx.constraintlayout.core.widgets.ConstraintWidget;
  import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashMap;
  
  public class Transition implements TypedValues {
      private static final boolean DEBUG = false;
      public static final int START = 0;
      public static final int END = 1;
      public static final int INTERPOLATED = 2;
      static final int EASE_IN_OUT = 0;
      static final int EASE_IN = 1;
      static final int EASE_OUT = 2;
      static final int LINEAR = 3;
      static final int BOUNCE = 4;
      static final int OVERSHOOT = 5;
      static final int ANTICIPATE = 6;
      private static final int SPLINE_STRING = -1;
      @SuppressWarnings("unused")
      private static final int INTERPOLATOR_REFERENCE_ID = -2;
      private HashMap<Integer, HashMap<String, KeyPosition>> mKeyPositions = new HashMap<>();
      private HashMap<String, WidgetState> mState = new HashMap<>();
      private TypedBundle mBundle = new TypedBundle();
      // Interpolation
      private int mDefaultInterpolator = 0;
      private String mDefaultInterpolatorString = null;
      private Easing mEasing = null;
      private int mAutoTransition = 0;
      private int mDuration = 400;
      private float mStagger = 0.0f;
      private OnSwipe mOnSwipe = null;
      final CorePixelDp mToPixel; // Todo placed here as a temp till the refactor is done
      int mParentStartWidth, mParentStartHeight;
      int mParentEndWidth, mParentEndHeight;
      int mParentInterpolatedWidth, mParentInterpolateHeight;
      boolean mWrap;
  
      public Transition(@NonNull CorePixelDp dpToPixel) {
          mToPixel = dpToPixel;
      }
  
      // @TODO: add description
      @SuppressWarnings("HiddenTypeParameter")
      OnSwipe createOnSwipe() {
          return mOnSwipe = new OnSwipe();
      }
  
      // @TODO: add description
      public boolean hasOnSwipe() {
          return mOnSwipe != null;
      }
  
      static class OnSwipe {
          String mAnchorId;
          private int mAnchorSide;
          private StopEngine mEngine;
          public static final int ANCHOR_SIDE_TOP = 0;
          public static final int ANCHOR_SIDE_LEFT = 1;
          public static final int ANCHOR_SIDE_RIGHT = 2;
          public static final int ANCHOR_SIDE_BOTTOM = 3;
          public static final int ANCHOR_SIDE_MIDDLE = 4;
          public static final int ANCHOR_SIDE_START = 5;
          public static final int ANCHOR_SIDE_END = 6;
          public static final String[] SIDES = {"top", "left", "right",
                  "bottom", "middle", "start", "end"};
          private static final float[][] TOUCH_SIDES = {
                  {0.5f, 0.0f}, // top
                  {0.0f, 0.5f}, // left
                  {1.0f, 0.5f}, // right
                  {0.5f, 1.0f}, // bottom
                  {0.5f, 0.5f}, // middle
                  {0.0f, 0.5f}, // start TODO (dynamically updated)
                  {1.0f, 0.5f}, // end  TODO (dynamically updated)
          };
  
          @SuppressWarnings("unused")
          private String mRotationCenterId;
          @SuppressWarnings("unused")
          private String mLimitBoundsTo;
          @SuppressWarnings("unused")
          private boolean mDragVertical = true;
          private int mDragDirection = 0;
          public static final int DRAG_UP = 0;
          public static final int DRAG_DOWN = 1;
          public static final int DRAG_LEFT = 2;
          public static final int DRAG_RIGHT = 3;
          public static final int DRAG_START = 4;
          public static final int DRAG_END = 5;
          public static final int DRAG_CLOCKWISE = 6;
          public static final int DRAG_ANTICLOCKWISE = 7;
          public static final String[] DIRECTIONS = {"up", "down", "left", "right", "start",
                  "end", "clockwise", "anticlockwise"};
  
          private float mDragScale = 1;
          @SuppressWarnings("unused")
          private float mDragThreshold = 10;
          private int mAutoCompleteMode = 0;
          public static final int MODE_CONTINUOUS_VELOCITY = 0;
          public static final int MODE_SPRING = 1;
          public static final String[] MODE = {"velocity", "spring"};
          private float mMaxVelocity = 4.f;
          private float mMaxAcceleration = 1.2f;
  
          // On touch up what happens
          private int mOnTouchUp = 0;
          public static final int ON_UP_AUTOCOMPLETE = 0;
          public static final int ON_UP_AUTOCOMPLETE_TO_START = 1;
          public static final int ON_UP_AUTOCOMPLETE_TO_END = 2;
          public static final int ON_UP_STOP = 3;
          public static final int ON_UP_DECELERATE = 4;
          public static final int ON_UP_DECELERATE_AND_COMPLETE = 5;
          public static final int ON_UP_NEVER_COMPLETE_TO_START = 6;
          public static final int ON_UP_NEVER_COMPLETE_TO_END = 7;
          public static final String[] TOUCH_UP = {"autocomplete", "toStart",
                  "toEnd", "stop", "decelerate", "decelerateComplete",
                  "neverCompleteStart", "neverCompleteEnd"};
  
          private float mSpringMass = 1;
          private float mSpringStiffness = 400;
          private float mSpringDamping = 10;
          private float mSpringStopThreshold = 0.01f;
          private float mDestination = 0.0f;
  
          // In spring mode what happens at the boundary
          private int mSpringBoundary = 0;
          public static final int BOUNDARY_OVERSHOOT = 0;
          public static final int BOUNDARY_BOUNCE_START = 1;
          public static final int BOUNDARY_BOUNCE_END = 2;
          public static final int BOUNDARY_BOUNCE_BOTH = 3;
          public static final String[] BOUNDARY = {"overshoot", "bounceStart",
                  "bounceEnd", "bounceBoth"};
  
          private static final float[][] TOUCH_DIRECTION = {
                  {0.0f, -1.0f}, // up
                  {0.0f, 1.0f}, // down
                  {-1.0f, 0.0f}, // left
                  {1.0f, 0.0f}, // right
                  {-1.0f, 0.0f}, // start (dynamically updated)
                  {1.0f, 0.0f}, // end  (dynamically updated)
          };
          private long mStart;
  
          float getScale() {
              return mDragScale;
          }
  
          float[] getDirection() {
              return TOUCH_DIRECTION[mDragDirection];
          }
  
          float[] getSide() {
              return TOUCH_SIDES[mAnchorSide];
          }
  
          void setAnchorId(String anchorId) {
              this.mAnchorId = anchorId;
          }
  
          void setAnchorSide(int anchorSide) {
              this.mAnchorSide = anchorSide;
          }
  
          void setRotationCenterId(String rotationCenterId) {
              this.mRotationCenterId = rotationCenterId;
          }
  
          void setLimitBoundsTo(String limitBoundsTo) {
              this.mLimitBoundsTo = limitBoundsTo;
          }
  
          void setDragDirection(int dragDirection) {
              this.mDragDirection = dragDirection;
              mDragVertical = (mDragDirection < 2);
          }
  
          void setDragScale(float dragScale) {
              if (Float.isNaN(dragScale)) {
                  return;
              }
              this.mDragScale = dragScale;
          }
  
          void setDragThreshold(float dragThreshold) {
              if (Float.isNaN(dragThreshold)) {
                  return;
              }
              this.mDragThreshold = dragThreshold;
          }
  
          void setAutoCompleteMode(int mAutoCompleteMode) {
              this.mAutoCompleteMode = mAutoCompleteMode;
          }
  
          void setMaxVelocity(float maxVelocity) {
              if (Float.isNaN(maxVelocity)) {
                  return;
              }
              this.mMaxVelocity = maxVelocity;
          }
  
          void setMaxAcceleration(float maxAcceleration) {
              if (Float.isNaN(maxAcceleration)) {
                  return;
              }
              this.mMaxAcceleration = maxAcceleration;
          }
  
          void setOnTouchUp(int onTouchUp) {
              this.mOnTouchUp = onTouchUp;
          }
  
          void setSpringMass(float mSpringMass) {
              if (Float.isNaN(mSpringMass)) {
                  return;
              }
              this.mSpringMass = mSpringMass;
          }
  
          void setSpringStiffness(float mSpringStiffness) {
              if (Float.isNaN(mSpringStiffness)) {
                  return;
              }
              this.mSpringStiffness = mSpringStiffness;
          }
  
          void setSpringDamping(float mSpringDamping) {
              if (Float.isNaN(mSpringDamping)) {
                  return;
              }
              this.mSpringDamping = mSpringDamping;
          }
  
          void setSpringStopThreshold(float mSpringStopThreshold) {
              if (Float.isNaN(mSpringStopThreshold)) {
                  return;
              }
              this.mSpringStopThreshold = mSpringStopThreshold;
          }
  
          void setSpringBoundary(int mSpringBoundary) {
              this.mSpringBoundary = mSpringBoundary;
          }
  
          float getDestinationPosition(float currentPosition, float velocity, float duration) {
              float rest = currentPosition + 0.5f * Math.abs(velocity) * velocity / mMaxAcceleration;
              switch (mOnTouchUp) {
                  case ON_UP_AUTOCOMPLETE_TO_START:
                  case ON_UP_NEVER_COMPLETE_TO_END:
                      return 0;
                  case ON_UP_AUTOCOMPLETE_TO_END:
                  case ON_UP_NEVER_COMPLETE_TO_START:
                      return 1;
                  case ON_UP_STOP:
                      return Float.NaN;
                  case ON_UP_DECELERATE:
                      return Math.max(0, Math.min(1, rest));
                  case ON_UP_DECELERATE_AND_COMPLETE: // complete if within 20% of edge #todo improve
                      if (rest > 0.2f && rest < 0.8f) {
                          return rest;
                      } else {
                          return rest > .5f ? 1 : 0;
                      }
                  case ON_UP_AUTOCOMPLETE:
              }
  
              if (DEBUG) {
                  Utils.log(" currentPosition = " + currentPosition);
                  Utils.log("        velocity = " + velocity);
                  Utils.log("            peek = " + rest);
                  Utils.log("mMaxAcceleration = " + mMaxAcceleration);
              }
              return rest > .5 ? 1 : 0;
          }
  
          void config(float position, float velocity, long start, float duration) {
              mStart = start;
              mDestination = getDestinationPosition(position, velocity, duration);
              if ((mOnTouchUp == ON_UP_DECELERATE)
                      && (mAutoCompleteMode == MODE_CONTINUOUS_VELOCITY)) {
                  StopLogicEngine.Decelerate sld;
                  if (mEngine instanceof StopLogicEngine.Decelerate) {
                      sld = (StopLogicEngine.Decelerate) mEngine;
                  } else {
                      mEngine = sld = new StopLogicEngine.Decelerate();
                  }
                  sld.config(position, mDestination, velocity);
                  return;
              }
  
  
              if (mAutoCompleteMode == MODE_CONTINUOUS_VELOCITY) {
                  StopLogicEngine sl;
                  if (mEngine instanceof StopLogicEngine) {
                      sl = (StopLogicEngine) mEngine;
                  } else {
                      mEngine = sl = new StopLogicEngine();
                  }
  
                  sl.config(position, mDestination, velocity,
                          duration, mMaxAcceleration,
                          mMaxVelocity);
                  return;
              }
              SpringStopEngine sl;
              if (mEngine instanceof SpringStopEngine) {
                  sl = (SpringStopEngine) mEngine;
              } else {
                  mEngine = sl = new SpringStopEngine();
              }
  
              sl.springConfig(position, mDestination, velocity,
                      mSpringMass,
                      mSpringStiffness,
                      mSpringDamping,
                      mSpringStopThreshold, mSpringBoundary);
          }
  
          /**
           * @param currentTime time in nanoseconds
           * @return new values of progress
           */
          public float getTouchUpProgress(long currentTime) {
              float time = (currentTime - mStart) * 1E-9f;
              float pos = mEngine.getInterpolation(time);
              if (mEngine.isStopped()) {
                  pos = mDestination;
              }
              return pos;
          }
  
          public void printInfo() {
              if (mAutoCompleteMode == MODE_CONTINUOUS_VELOCITY) {
                  System.out.println("velocity = " + mEngine.getVelocity());
                  System.out.println("mMaxAcceleration = " + mMaxAcceleration);
                  System.out.println("mMaxVelocity = " + mMaxVelocity);
              } else {
                  System.out.println("mSpringMass          = " + mSpringMass);
                  System.out.println("mSpringStiffness     = " + mSpringStiffness);
                  System.out.println("mSpringDamping       = " + mSpringDamping);
                  System.out.println("mSpringStopThreshold = " + mSpringStopThreshold);
                  System.out.println("mSpringBoundary      = " + mSpringBoundary);
              }
          }
  
          public boolean isNotDone(float progress) {
              if (mOnTouchUp == ON_UP_STOP) {
                  return false;
              }
              return !mEngine.isStopped();
          }
      }
  
      /**
       * Converts from xy drag to progress
       * This should be used till touch up
       *
       * @param baseW parent width
       * @param baseH parent height
       * @param dx    change in x
       * @param dy    change in y
       * @return the change in progress
       */
      public float dragToProgress(float currentProgress, int baseW, int baseH, float dx, float dy) {
          Collection<WidgetState> widgets = mState.values();
          WidgetState childWidget = null;
          for (WidgetState widget : widgets) {
              childWidget = widget;
              break;
          }
          if (mOnSwipe == null || childWidget == null) {
              if (childWidget != null) {
                  return -dy / childWidget.mParentHeight;
              }
              return 1.0f;
          }
          if (mOnSwipe.mAnchorId == null) {
  
              float[] dir = mOnSwipe.getDirection();
              float motionDpDtX = childWidget.mParentHeight;
              float motionDpDtY = childWidget.mParentHeight;
  
              float drag = (dir[0] != 0) ? dx * Math.abs(dir[0]) / motionDpDtX
                      : dy * Math.abs(dir[1]) / motionDpDtY;
              return drag * mOnSwipe.getScale();
          }
          WidgetState base = mState.get(mOnSwipe.mAnchorId);
          float[] dir = mOnSwipe.getDirection();
          float[] side = mOnSwipe.getSide();
          float[] motionDpDt = new float[2];
  
          base.interpolate(baseW, baseH, currentProgress, this);
          base.mMotionControl.getDpDt(currentProgress, side[0], side[1], motionDpDt);
          float drag = (dir[0] != 0) ? dx * Math.abs(dir[0]) / motionDpDt[0]
                  : dy * Math.abs(dir[1]) / motionDpDt[1];
          if (DEBUG) {
              Utils.log(" drag " + drag);
          }
          return drag * mOnSwipe.getScale();
      }
  
      /**
       * Set the start of the touch up
       *
       * @param currentProgress 0...1 progress in
       * @param currentTime     time in nanoseconds
       * @param velocityX       pixels per millisecond
       * @param velocityY       pixels per millisecond
       */
      public void setTouchUp(float currentProgress,
              long currentTime,
              float velocityX,
              float velocityY) {
          if (mOnSwipe != null) {
              if (DEBUG) {
                  Utils.log(" >>> velocity x,y = " + velocityX + " , " + velocityY);
              }
              WidgetState base = mState.get(mOnSwipe.mAnchorId);
              float[] motionDpDt = new float[2];
              float[] dir = mOnSwipe.getDirection();
              float[] side = mOnSwipe.getSide();
              base.mMotionControl.getDpDt(currentProgress, side[0], side[1], motionDpDt);
              float movementInDir = dir[0] * motionDpDt[0] + dir[1] * motionDpDt[1];
              if (Math.abs(movementInDir) < 0.01) {
                  if (DEBUG) {
                      Utils.log(" >>> cap minimum v!! ");
                  }
                  motionDpDt[0] = .01f;
                  motionDpDt[1] = .01f;
              }
  
              float drag = (dir[0] != 0) ? velocityX / motionDpDt[0] : velocityY / motionDpDt[1];
              drag *= mOnSwipe.getScale();
              if (DEBUG) {
                  Utils.log(" >>> velocity        " + drag);
                  Utils.log(" >>> mDuration       " + mDuration);
                  Utils.log(" >>> currentProgress " + currentProgress);
              }
              mOnSwipe.config(currentProgress, drag, currentTime, mDuration * 1E-3f);
              if (DEBUG) {
                  mOnSwipe.printInfo();
              }
          }
      }
  
      /**
       * get the current touch up progress current time in nanoseconds
       * (ideally coming from an animation clock)
       *
       * @param currentTime in nanoseconds
       * @return progress
       */
      public float getTouchUpProgress(long currentTime) {
          if (mOnSwipe != null) {
              return mOnSwipe.getTouchUpProgress(currentTime);
          }
          return 0;
      }
  
      /**
       * Are we still animating
       *
       * @param currentProgress motion progress
       * @return true to continue moving
       */
      public boolean isTouchNotDone(float currentProgress) {
          return mOnSwipe.isNotDone(currentProgress);
      }
  
      /**
       * get the interpolater based on a constant or a string
       */
      public static Interpolator getInterpolator(int interpolator, String interpolatorString) {
          switch (interpolator) {
              case SPLINE_STRING:
                  return v -> (float) Easing.getInterpolator(interpolatorString).get(v);
              case EASE_IN_OUT:
                  return v -> (float) Easing.getInterpolator("standard").get(v);
              case EASE_IN:
                  return v -> (float) Easing.getInterpolator("accelerate").get(v);
              case EASE_OUT:
                  return v -> (float) Easing.getInterpolator("decelerate").get(v);
              case LINEAR:
                  return v -> (float) Easing.getInterpolator("linear").get(v);
              case ANTICIPATE:
                  return v -> (float) Easing.getInterpolator("anticipate").get(v);
              case OVERSHOOT:
                  return v -> (float) Easing.getInterpolator("overshoot").get(v);
              case BOUNCE: // TODO make a better bounce
                  return v -> (float) Easing.getInterpolator("spline(0.0, 0.2, 0.4, 0.6, "
                          + "0.8 ,1.0, 0.8, 1.0, 0.9, 1.0)").get(v);
          }
          return null;
      }
  
      // @TODO: add description
      @SuppressWarnings("HiddenTypeParameter")
      public KeyPosition findPreviousPosition(String target, int frameNumber) {
          while (frameNumber >= 0) {
              HashMap<String, KeyPosition> map = mKeyPositions.get(frameNumber);
              if (map != null) {
                  KeyPosition keyPosition = map.get(target);
                  if (keyPosition != null) {
                      return keyPosition;
                  }
              }
              frameNumber--;
          }
          return null;
      }
  
      // @TODO: add description
      @SuppressWarnings("HiddenTypeParameter")
      public KeyPosition findNextPosition(String target, int frameNumber) {
          while (frameNumber <= 100) {
              HashMap<String, KeyPosition> map = mKeyPositions.get(frameNumber);
              if (map != null) {
                  KeyPosition keyPosition = map.get(target);
                  if (keyPosition != null) {
                      return keyPosition;
                  }
              }
              frameNumber++;
          }
          return null;
      }
  
      // @TODO: add description
      public int getNumberKeyPositions(WidgetFrame frame) {
          int numKeyPositions = 0;
          int frameNumber = 0;
          while (frameNumber <= 100) {
              HashMap<String, KeyPosition> map = mKeyPositions.get(frameNumber);
              if (map != null) {
                  KeyPosition keyPosition = map.get(frame.widget.stringId);
                  if (keyPosition != null) {
                      numKeyPositions++;
                  }
              }
              frameNumber++;
          }
          return numKeyPositions;
      }
  
      // @TODO: add description
      public Motion getMotion(String id) {
          return getWidgetState(id, null, 0).mMotionControl;
      }
  
      // @TODO: add description
      public void fillKeyPositions(WidgetFrame frame, float[] x, float[] y, float[] pos) {
          int numKeyPositions = 0;
          int frameNumber = 0;
          while (frameNumber <= 100) {
              HashMap<String, KeyPosition> map = mKeyPositions.get(frameNumber);
              if (map != null) {
                  KeyPosition keyPosition = map.get(frame.widget.stringId);
                  if (keyPosition != null) {
                      x[numKeyPositions] = keyPosition.mX;
                      y[numKeyPositions] = keyPosition.mY;
                      pos[numKeyPositions] = keyPosition.mFrame;
                      numKeyPositions++;
                  }
              }
              frameNumber++;
          }
      }
  
      // @TODO: add description
      public boolean hasPositionKeyframes() {
          return mKeyPositions.size() > 0;
      }
  
      // @TODO: add description
      public void setTransitionProperties(TypedBundle bundle) {
          bundle.applyDelta(mBundle);
          bundle.applyDelta(this);
      }
  
      @Override
      public boolean setValue(int id, int value) {
          return false;
      }
  
      @Override
      public boolean setValue(int id, float value) {
          if (id == TypedValues.TransitionType.TYPE_STAGGERED) {
              mStagger = value;
          }
          return false;
      }
  
      @Override
      public boolean setValue(int id, String value) {
          if (id == TransitionType.TYPE_INTERPOLATOR) {
              mEasing = Easing.getInterpolator(mDefaultInterpolatorString = value);
          }
          return false;
      }
  
      @Override
      public boolean setValue(int id, boolean value) {
          return false;
      }
  
      @Override
      public int getId(String name) {
          return 0;
      }
  
      public boolean isEmpty() {
          return mState.isEmpty();
      }
  
      // @TODO: add description
      public void clear() {
          mState.clear();
      }
  
      // @TODO: add description
      public boolean contains(String key) {
          return mState.containsKey(key);
      }
  
      // @TODO: add description
      public void addKeyPosition(String target, TypedBundle bundle) {
          getWidgetState(target, null, 0).setKeyPosition(bundle);
      }
  
      // @TODO: add description
      public void addKeyAttribute(String target, TypedBundle bundle) {
          getWidgetState(target, null, 0).setKeyAttribute(bundle);
      }
  
      /**
       * Add a key attribute and the custom variables into the
       * @param target the id of the target
       * @param bundle the key attributes bundle containing position etc.
       * @param custom the customVariables to add at that position
       */
      public void addKeyAttribute(String target, TypedBundle bundle, CustomVariable[]custom) {
          getWidgetState(target, null, 0).setKeyAttribute(bundle,custom);
      }
  
      // @TODO: add description
      public void addKeyCycle(String target, TypedBundle bundle) {
          getWidgetState(target, null, 0).setKeyCycle(bundle);
      }
  
      // @TODO: add description
      public void addKeyPosition(String target, int frame, int type, float x, float y) {
          TypedBundle bundle = new TypedBundle();
          bundle.add(TypedValues.PositionType.TYPE_POSITION_TYPE, 2);
          bundle.add(TypedValues.TYPE_FRAME_POSITION, frame);
          bundle.add(TypedValues.PositionType.TYPE_PERCENT_X, x);
          bundle.add(TypedValues.PositionType.TYPE_PERCENT_Y, y);
          getWidgetState(target, null, 0).setKeyPosition(bundle);
  
          KeyPosition keyPosition = new KeyPosition(target, frame, type, x, y);
          HashMap<String, KeyPosition> map = mKeyPositions.get(frame);
          if (map == null) {
              map = new HashMap<>();
              mKeyPositions.put(frame, map);
          }
          map.put(target, keyPosition);
      }
  
      // @TODO: add description
      public void addCustomFloat(int state, String widgetId, String property, float value) {
          WidgetState widgetState = getWidgetState(widgetId, null, state);
          WidgetFrame frame = widgetState.getFrame(state);
          frame.addCustomFloat(property, value);
      }
  
      // @TODO: add description
      public void addCustomColor(int state, String widgetId, String property, int color) {
          WidgetState widgetState = getWidgetState(widgetId, null, state);
          WidgetFrame frame = widgetState.getFrame(state);
          frame.addCustomColor(property, color);
      }
  
      private void calculateParentDimensions(float progress) {
          mParentInterpolatedWidth = (int) (0.5f +
                  mParentStartWidth + (mParentEndWidth - mParentStartWidth) * progress);
          mParentInterpolateHeight = (int) (0.5f +
                  mParentStartHeight + (mParentEndHeight - mParentStartHeight) * progress);
      }
  
      public int getInterpolatedWidth() {
          return mParentInterpolatedWidth;
      }
  
      public int getInterpolatedHeight() {
          return mParentInterpolateHeight;
      }
      /**
       * Update container of parameters for the state
       *
       * @param container contains all the widget parameters
       * @param state     starting or ending
       */
      public void updateFrom(ConstraintWidgetContainer container, int state) {
          mWrap = container.mListDimensionBehaviors[0]
                  == ConstraintWidget.DimensionBehaviour.WRAP_CONTENT;
          mWrap |= container.mListDimensionBehaviors[1]
                  == ConstraintWidget.DimensionBehaviour.WRAP_CONTENT;
          if (state == START) {
              mParentInterpolatedWidth = mParentStartWidth = container.getWidth();
              mParentInterpolateHeight = mParentStartHeight = container.getHeight();
          } else {
              mParentEndWidth = container.getWidth();
              mParentEndHeight = container.getHeight();
          }
          final ArrayList<ConstraintWidget> children = container.getChildren();
          final int count = children.size();
          WidgetState[] states = new WidgetState[count];
  
          for (int i = 0; i < count; i++) {
              ConstraintWidget child = children.get(i);
              WidgetState widgetState = getWidgetState(child.stringId, null, state);
              states[i] = widgetState;
              widgetState.update(child, state);
              String id = widgetState.getPathRelativeId();
              if (id != null) {
                  widgetState.setPathRelative(getWidgetState(id, null, state));
              }
          }
  
          calcStagger();
      }
  
      // @TODO: add description
      public void interpolate(int parentWidth, int parentHeight, float progress) {
          if (mWrap) {
              calculateParentDimensions(progress);
          }
  
          if (mEasing != null) {
              progress = (float) mEasing.get(progress);
          }
          for (String key : mState.keySet()) {
              WidgetState widget = mState.get(key);
              widget.interpolate(parentWidth, parentHeight, progress, this);
          }
      }
  
      // @TODO: add description
      public WidgetFrame getStart(String id) {
          WidgetState widgetState = mState.get(id);
          if (widgetState == null) {
              return null;
          }
          return widgetState.mStart;
      }
  
      // @TODO: add description
      public WidgetFrame getEnd(String id) {
          WidgetState widgetState = mState.get(id);
          if (widgetState == null) {
              return null;
          }
          return widgetState.mEnd;
      }
  
      // @TODO: add description
      public WidgetFrame getInterpolated(String id) {
          WidgetState widgetState = mState.get(id);
          if (widgetState == null) {
              return null;
          }
          return widgetState.mInterpolated;
      }
  
      // @TODO: add description
      public float[] getPath(String id) {
          WidgetState widgetState = mState.get(id);
          int duration = 1000;
          int frames = duration / 16;
          float[] mPoints = new float[frames * 2];
          widgetState.mMotionControl.buildPath(mPoints, frames);
          return mPoints;
      }
  
      // @TODO: add description
      public int getKeyFrames(String id, float[] rectangles, int[] pathMode, int[] position) {
          WidgetState widgetState = mState.get(id);
          return widgetState.mMotionControl.buildKeyFrames(rectangles, pathMode, position);
      }
  
      @SuppressWarnings("unused")
      private WidgetState getWidgetState(String widgetId) {
          return this.mState.get(widgetId);
      }
  
      public WidgetState getWidgetState(String widgetId,
              ConstraintWidget child,
              int transitionState) {
          WidgetState widgetState = this.mState.get(widgetId);
          if (widgetState == null) {
              widgetState = new WidgetState();
              mBundle.applyDelta(widgetState.mMotionControl);
              widgetState.mMotionWidgetStart.updateMotion(widgetState.mMotionControl);
              mState.put(widgetId, widgetState);
              if (child != null) {
                  widgetState.update(child, transitionState);
              }
          }
          return widgetState;
      }
  
      /**
       * Used in debug draw
       */
      public WidgetFrame getStart(ConstraintWidget child) {
          return getWidgetState(child.stringId, null, Transition.START).mStart;
      }
  
      /**
       * Used in debug draw
       */
      public WidgetFrame getEnd(ConstraintWidget child) {
          return getWidgetState(child.stringId, null, Transition.END).mEnd;
      }
  
      /**
       * Used after the interpolation
       */
      public WidgetFrame getInterpolated(ConstraintWidget child) {
          return getWidgetState(child.stringId, null, Transition.INTERPOLATED).mInterpolated;
      }
  
      /**
       * This gets the interpolator being used
       */
      public Interpolator getInterpolator() {
          return getInterpolator(mDefaultInterpolator, mDefaultInterpolatorString);
      }
  
      /**
       * This gets the auto transition mode being used
       */
      public int getAutoTransition() {
          return mAutoTransition;
      }
  
      public static class WidgetState {
          WidgetFrame mStart;
          WidgetFrame mEnd;
          WidgetFrame mInterpolated;
          Motion mMotionControl;
          boolean mNeedSetup = true;
          MotionWidget mMotionWidgetStart;
          MotionWidget mMotionWidgetEnd;
          MotionWidget mMotionWidgetInterpolated;
          KeyCache mKeyCache = new KeyCache();
          int mParentHeight = -1;
          int mParentWidth = -1;
  
          public WidgetState() {
              mStart = new WidgetFrame();
              mEnd = new WidgetFrame();
              mInterpolated = new WidgetFrame();
              mMotionWidgetStart = new MotionWidget(mStart);
              mMotionWidgetEnd = new MotionWidget(mEnd);
              mMotionWidgetInterpolated = new MotionWidget(mInterpolated);
              mMotionControl = new Motion(mMotionWidgetStart);
              mMotionControl.setStart(mMotionWidgetStart);
              mMotionControl.setEnd(mMotionWidgetEnd);
          }
  
          public void setKeyPosition(TypedBundle prop) {
              MotionKeyPosition keyPosition = new MotionKeyPosition();
              prop.applyDelta(keyPosition);
              mMotionControl.addKey(keyPosition);
          }
  
          public void setKeyAttribute(TypedBundle prop) {
              MotionKeyAttributes keyAttributes = new MotionKeyAttributes();
              prop.applyDelta(keyAttributes);
              mMotionControl.addKey(keyAttributes);
          }
  
          /**
           * Set tge keyAttribute bundle and associated custom attributes
           * @param prop
           * @param custom
           */
          public void setKeyAttribute(TypedBundle prop, CustomVariable[] custom) {
              MotionKeyAttributes keyAttributes = new MotionKeyAttributes();
              prop.applyDelta(keyAttributes);
              if (custom != null) {
                  for (int i = 0; i < custom.length; i++) {
                      keyAttributes.mCustom.put( custom[i].getName(), custom[i]);
                  }
              }
              mMotionControl.addKey(keyAttributes);
          }
  
          public void setKeyCycle(TypedBundle prop) {
              MotionKeyCycle keyAttributes = new MotionKeyCycle();
              prop.applyDelta(keyAttributes);
              mMotionControl.addKey(keyAttributes);
          }
  
          public void update(ConstraintWidget child, int state) {
              if (state == START) {
                  mStart.update(child);
                  mMotionWidgetStart.updateMotion(mMotionWidgetStart);
                  mMotionControl.setStart(mMotionWidgetStart);
                  mNeedSetup = true;
              } else if (state == END) {
                  mEnd.update(child);
                  mMotionControl.setEnd(mMotionWidgetEnd);
                  mNeedSetup = true;
              }
              mParentWidth = -1;
          }
  
          /**
           * Return the id of the widget to animate relative to
           *
           * @return id of widget or null
           */
          String getPathRelativeId() {
              return mMotionControl.getAnimateRelativeTo();
          }
  
          public WidgetFrame getFrame(int type) {
              if (type == START) {
                  return mStart;
              } else if (type == END) {
                  return mEnd;
              }
              return mInterpolated;
          }
  
          public void interpolate(int parentWidth,
                  int parentHeight,
                  float progress,
                  Transition transition) {
              // TODO  only update if parentHeight != mParentHeight || parentWidth != mParentWidth) {
              mParentHeight = parentHeight;
              mParentWidth = parentWidth;
              if (mNeedSetup) {
                  mMotionControl.setup(parentWidth, parentHeight, 1, System.nanoTime());
                  mNeedSetup = false;
              }
              WidgetFrame.interpolate(parentWidth, parentHeight,
                      mInterpolated, mStart, mEnd, transition, progress);
              mInterpolated.interpolatedPos = progress;
              mMotionControl.interpolate(mMotionWidgetInterpolated,
                      progress, System.nanoTime(), mKeyCache);
          }
  
          public void setPathRelative(WidgetState widgetState) {
              mMotionControl.setupRelative(widgetState.mMotionControl);
          }
      }
  
      static class KeyPosition {
          int mFrame;
          String mTarget;
          int mType;
          float mX;
          float mY;
  
          KeyPosition(String target, int frame, int type, float x, float y) {
              this.mTarget = target;
              this.mFrame = frame;
              this.mType = type;
              this.mX = x;
              this.mY = y;
          }
      }
  
      public void calcStagger() {
          if (mStagger == 0.0f) {
              return;
          }
          boolean flip = mStagger < 0.0;
  
          float stagger = Math.abs(mStagger);
          float min = Float.MAX_VALUE, max = -Float.MAX_VALUE;
          boolean useMotionStagger = false;
  
          for (String widgetId : mState.keySet()) {
              WidgetState widgetState = mState.get(widgetId);
              Motion f = widgetState.mMotionControl;
              if (!Float.isNaN(f.getMotionStagger())) {
                  useMotionStagger = true;
                  break;
              }
          }
          if (useMotionStagger) {
              for (String widgetId : mState.keySet()) {
                  WidgetState widgetState = mState.get(widgetId);
                  Motion f = widgetState.mMotionControl;
                  float widgetStagger = f.getMotionStagger();
                  if (!Float.isNaN(widgetStagger)) {
                      min = Math.min(min, widgetStagger);
                      max = Math.max(max, widgetStagger);
                  }
              }
  
              for (String widgetId : mState.keySet()) {
                  WidgetState widgetState = mState.get(widgetId);
                  Motion f = widgetState.mMotionControl;
  
                  float widgetStagger = f.getMotionStagger();
                  if (!Float.isNaN(widgetStagger)) {
                      float scale = 1 / (1 - stagger);
  
                      float offset = stagger - stagger * (widgetStagger - min) / (max - min);
                      if (flip) {
                          offset = stagger - stagger
                                  * ((max - widgetStagger) / (max - min));
                      }
                      f.setStaggerScale(scale);
                      f.setStaggerOffset(offset);
                  }
              }
  
          } else {
              for (String widgetId : mState.keySet()) {
                  WidgetState widgetState = mState.get(widgetId);
                  Motion f = widgetState.mMotionControl;
                  float x = f.getFinalX();
                  float y = f.getFinalY();
                  float widgetStagger = x + y;
                  min = Math.min(min, widgetStagger);
                  max = Math.max(max, widgetStagger);
              }
  
              for (String widgetId : mState.keySet()) {
                  WidgetState widgetState = mState.get(widgetId);
                  Motion f = widgetState.mMotionControl;
                  float x = f.getFinalX();
                  float y = f.getFinalY();
                  float widgetStagger = x + y;
                  float offset = stagger - stagger * (widgetStagger - min) / (max - min);
                  if (flip) {
                      offset = stagger - stagger
                              * ((max - widgetStagger) / (max - min));
                  }
  
                  float scale = 1 / (1 - stagger);
                  f.setStaggerScale(scale);
                  f.setStaggerOffset(offset);
              }
          }
      }
  }
  