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.motion;
  
  import static androidx.constraintlayout.core.motion.MotionWidget.UNSET;
  
  import androidx.constraintlayout.core.motion.key.MotionConstraintSet;
  import androidx.constraintlayout.core.motion.key.MotionKey;
  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.key.MotionKeyTimeCycle;
  import androidx.constraintlayout.core.motion.key.MotionKeyTrigger;
  import androidx.constraintlayout.core.motion.utils.CurveFit;
  import androidx.constraintlayout.core.motion.utils.DifferentialInterpolator;
  import androidx.constraintlayout.core.motion.utils.Easing;
  import androidx.constraintlayout.core.motion.utils.FloatRect;
  import androidx.constraintlayout.core.motion.utils.KeyCache;
  import androidx.constraintlayout.core.motion.utils.KeyCycleOscillator;
  import androidx.constraintlayout.core.motion.utils.KeyFrameArray;
  import androidx.constraintlayout.core.motion.utils.Rect;
  import androidx.constraintlayout.core.motion.utils.SplineSet;
  import androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet;
  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.motion.utils.VelocityMatrix;
  import androidx.constraintlayout.core.motion.utils.ViewState;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  
  /**
   * Contains the picture of a view through a transition and is used to interpolate it.
   * During a transition every view has a MotionController which drives its position.
   * <p>
   * All parameter which affect a views motion are added to MotionController and then setup()
   * builds out the splines that control the view.
   *
   *
   */
  public class Motion implements TypedValues {
      public static final int PATH_PERCENT = 0;
      public static final int PATH_PERPENDICULAR = 1;
      public static final int HORIZONTAL_PATH_X = 2;
      public static final int HORIZONTAL_PATH_Y = 3;
      public static final int VERTICAL_PATH_X = 4;
      public static final int VERTICAL_PATH_Y = 5;
      public static final int DRAW_PATH_NONE = 0;
      public static final int DRAW_PATH_BASIC = 1;
      public static final int DRAW_PATH_RELATIVE = 2;
      public static final int DRAW_PATH_CARTESIAN = 3;
      public static final int DRAW_PATH_AS_CONFIGURED = 4;
      public static final int DRAW_PATH_RECTANGLE = 5;
      public static final int DRAW_PATH_SCREEN = 6;
  
      public static final int ROTATION_RIGHT = 1;
      public static final int ROTATION_LEFT = 2;
      Rect mTempRect = new Rect(); // for efficiency
  
      private static final String TAG = "MotionController";
      private static final boolean DEBUG = false;
      private static final boolean FAVOR_FIXED_SIZE_VIEWS = false;
      MotionWidget mView;
      public String mId;
      String mConstraintTag;
      private int mCurveFitType = CurveFit.SPLINE;
      private MotionPaths mStartMotionPath = new MotionPaths();
      private MotionPaths mEndMotionPath = new MotionPaths();
  
      private MotionConstrainedPoint mStartPoint = new MotionConstrainedPoint();
      private MotionConstrainedPoint mEndPoint = new MotionConstrainedPoint();
  
      // spline 0 is the generic one that process all the standard attributes
      private CurveFit[] mSpline;
      private CurveFit mArcSpline;
      float mMotionStagger = Float.NaN;
      float mStaggerOffset = 0;
      float mStaggerScale = 1.0f;
      float mCurrentCenterX, mCurrentCenterY;
      private int[] mInterpolateVariables;
      private double[] mInterpolateData; // scratch data created during setup
      private double[] mInterpolateVelocity; // scratch data created during setup
  
      private String[] mAttributeNames;  // the names of the custom attributes
      private int[] mAttributeInterpolatorCount; // how many interpolators for each custom attribute
      private int mMaxDimension = 4;
      private float[] mValuesBuff = new float[mMaxDimension];
      private ArrayList<MotionPaths> mMotionPaths = new ArrayList<>();
      private float[] mVelocity = new float[1]; // used as a temp buffer to return values
  
      private ArrayList<MotionKey> mKeyList = new ArrayList<>(); // List of key frame items
  
      // splines to calculate for use TimeCycles
      private HashMap<String, TimeCycleSplineSet> mTimeCycleAttributesMap;
      private HashMap<String, SplineSet> mAttributesMap; // splines to calculate values of attributes
  
      // splines to calculate values of attributes
      private HashMap<String, KeyCycleOscillator> mCycleMap;
      private MotionKeyTrigger[] mKeyTriggers; // splines to calculate values of attributes
      private int mPathMotionArc = UNSET;
  
      // if set, pivot point is maintained as the other object
      private int mTransformPivotTarget = UNSET;
  
      // if set, pivot point is maintained as the other object
      private MotionWidget mTransformPivotView = null;
      private int mQuantizeMotionSteps = UNSET;
      private float mQuantizeMotionPhase = Float.NaN;
      private DifferentialInterpolator mQuantizeMotionInterpolator = null;
      private boolean mNoMovement = false;
      Motion mRelativeMotion;
      /**
       * Get the view to pivot around
       *
       * @return id of view or UNSET if not set
       */
      public int getTransformPivotTarget() {
          return mTransformPivotTarget;
      }
  
      /**
       * Set a view to pivot around
       *
       * @param transformPivotTarget id of view
       */
      public void setTransformPivotTarget(int transformPivotTarget) {
          mTransformPivotTarget = transformPivotTarget;
          mTransformPivotView = null;
      }
  
      /**
       * provides access to MotionPath objects
       */
      public MotionPaths getKeyFrame(int i) {
          return mMotionPaths.get(i);
      }
  
      public Motion(MotionWidget view) {
          setView(view);
      }
  
      /**
       * get the left most position of the widget at the start of the movement.
       *
       * @return the left most position
       */
      public float getStartX() {
          return mStartMotionPath.mX;
      }
  
      /**
       * get the top most position of the widget at the start of the movement.
       * Positive is down.
       *
       * @return the top most position
       */
      public float getStartY() {
          return mStartMotionPath.mY;
      }
  
      /**
       * get the left most position of the widget at the end of the movement.
       *
       * @return the left most position
       */
      public float getFinalX() {
          return mEndMotionPath.mX;
      }
  
      /**
       * get the top most position of the widget at the end of the movement.
       * Positive is down.
       *
       * @return the top most position
       */
      public float getFinalY() {
          return mEndMotionPath.mY;
      }
  
      /**
       * get the width of the widget at the start of the movement.
       *
       * @return the width at the start
       */
      public float getStartWidth() {
          return mStartMotionPath.mWidth;
      }
  
      /**
       * get the width of the widget at the start of the movement.
       *
       * @return the height at the start
       */
      public float getStartHeight() {
          return mStartMotionPath.mHeight;
      }
  
      /**
       * get the width of the widget at the end of the movement.
       *
       * @return the width at the end
       */
      public float getFinalWidth() {
          return mEndMotionPath.mWidth;
      }
  
      /**
       * get the width of the widget at the end of the movement.
       *
       * @return the height at the end
       */
      public float getFinalHeight() {
          return mEndMotionPath.mHeight;
      }
  
      /**
       * Returns the id of the view to move relative to.
       * The position at the start and then end will be viewed relative to this view
       * -1 is the return value if NOT in polar mode
       *
       * @return the view id of the view this is in polar mode to or -1 if not in polar
       */
      public String getAnimateRelativeTo() {
          return mStartMotionPath.mAnimateRelativeTo;
      }
  
      /**
       * set up the motion to be relative to this other motionController
       */
      public void setupRelative(Motion motionController) {
          mRelativeMotion = motionController;
      }
  
      private void setupRelative() {
          if (mRelativeMotion == null) {
              return;
          }
          mStartMotionPath.setupRelative(mRelativeMotion, mRelativeMotion.mStartMotionPath);
          mEndMotionPath.setupRelative(mRelativeMotion, mRelativeMotion.mEndMotionPath);
      }
  
      public float getCenterX() {
          return mCurrentCenterX;
      }
  
      public float getCenterY() {
          return mCurrentCenterY;
      }
  
      // @TODO: add description
      public void getCenter(double p, float[] pos, float[] vel) {
          double[] position = new double[4];
          double[] velocity = new double[4];
          mSpline[0].getPos(p, position);
          mSpline[0].getSlope(p, velocity);
          Arrays.fill(vel, 0);
          mStartMotionPath.getCenter(p, mInterpolateVariables, position, pos, velocity, vel);
      }
  
      /**
       * Fills the array point with the center coordinates.
       * point[0] is filled with the
       * x coordinate of "time" 0.0 mPoints[point.length-1] is filled with the y coordinate of "time"
       * 1.0
       *
       * @param points array to fill (should be 2x the number of mPoints
       */
      public void buildPath(float[] points, int pointCount) {
          float mils = 1.0f / (pointCount - 1);
          SplineSet trans_x =
                  (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.TRANSLATION_X);
          SplineSet trans_y =
                  (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.TRANSLATION_Y);
          KeyCycleOscillator osc_x =
                  (mCycleMap == null) ? null : mCycleMap.get(MotionKey.TRANSLATION_X);
          KeyCycleOscillator osc_y =
                  (mCycleMap == null) ? null : mCycleMap.get(MotionKey.TRANSLATION_Y);
  
          for (int i = 0; i < pointCount; i++) {
              float position = i * mils;
              if (mStaggerScale != 1.0f) {
                  if (position < mStaggerOffset) {
                      position = 0;
                  }
                  if (position > mStaggerOffset && position < 1.0) {
                      position -= mStaggerOffset;
                      position *= mStaggerScale;
                      position = Math.min(position, 1.0f);
                  }
              }
              double p = position;
  
              Easing easing = mStartMotionPath.mKeyFrameEasing;
              float start = 0;
              float end = Float.NaN;
              for (MotionPaths frame : mMotionPaths) {
                  if (frame.mKeyFrameEasing != null) { // this frame has an easing
                      if (frame.mTime < position) {  // frame with easing is before the current pos
                          easing = frame.mKeyFrameEasing; // this is the candidate
                          start = frame.mTime; // this is also the starting time
                      } else { // frame with easing is past the pos
                          if (Float.isNaN(end)) { // we never ended the time line
                              end = frame.mTime;
                          }
                      }
                  }
              }
  
              if (easing != null) {
                  if (Float.isNaN(end)) {
                      end = 1.0f;
                  }
                  float offset = (position - start) / (end - start);
                  offset = (float) easing.get(offset);
                  p = offset * (end - start) + start;
  
              }
  
              mSpline[0].getPos(p, mInterpolateData);
              if (mArcSpline != null) {
                  if (mInterpolateData.length > 0) {
                      mArcSpline.getPos(p, mInterpolateData);
                  }
              }
              mStartMotionPath.getCenter(p, mInterpolateVariables, mInterpolateData, points, i * 2);
  
              if (osc_x != null) {
                  points[i * 2] += osc_x.get(position);
              } else if (trans_x != null) {
                  points[i * 2] += trans_x.get(position);
              }
              if (osc_y != null) {
                  points[i * 2 + 1] += osc_y.get(position);
              } else if (trans_y != null) {
                  points[i * 2 + 1] += trans_y.get(position);
              }
          }
      }
  
      double[] getPos(double position) {
          mSpline[0].getPos(position, mInterpolateData);
          if (mArcSpline != null) {
              if (mInterpolateData.length > 0) {
                  mArcSpline.getPos(position, mInterpolateData);
              }
          }
          return mInterpolateData;
      }
  
      /**
       * Fills the array point with the center coordinates.
       * point[0] is filled with the
       * x coordinate of "time" 0.0 mPoints[point.length-1] is filled with the y coordinate of "time"
       * 1.0
       *
       * @param bounds array to fill (should be 2x the number of mPoints
       * @return number of key frames
       */
      void buildBounds(float[] bounds, int pointCount) {
          float mils = 1.0f / (pointCount - 1);
          @SuppressWarnings("unused") SplineSet trans_x =
                  (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.TRANSLATION_X);
          @SuppressWarnings("unused") SplineSet trans_y =
                  (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.TRANSLATION_Y);
          @SuppressWarnings("unused") KeyCycleOscillator osc_x =
                  (mCycleMap == null) ? null : mCycleMap.get(MotionKey.TRANSLATION_X);
          @SuppressWarnings("unused") KeyCycleOscillator osc_y =
                  (mCycleMap == null) ? null : mCycleMap.get(MotionKey.TRANSLATION_Y);
  
          for (int i = 0; i < pointCount; i++) {
              float position = i * mils;
              if (mStaggerScale != 1.0f) {
                  if (position < mStaggerOffset) {
                      position = 0;
                  }
                  if (position > mStaggerOffset && position < 1.0) {
                      position -= mStaggerOffset;
                      position *= mStaggerScale;
                      position = Math.min(position, 1.0f);
                  }
              }
              double p = position;
  
              Easing easing = mStartMotionPath.mKeyFrameEasing;
              float start = 0;
              float end = Float.NaN;
              for (MotionPaths frame : mMotionPaths) {
                  if (frame.mKeyFrameEasing != null) { // this frame has an easing
                      if (frame.mTime < position) {  // frame with easing is before the current pos
                          easing = frame.mKeyFrameEasing; // this is the candidate
                          start = frame.mTime; // this is also the starting time
                      } else { // frame with easing is past the pos
                          if (Float.isNaN(end)) { // we never ended the time line
                              end = frame.mTime;
                          }
                      }
                  }
              }
  
              if (easing != null) {
                  if (Float.isNaN(end)) {
                      end = 1.0f;
                  }
                  float offset = (position - start) / (end - start);
                  offset = (float) easing.get(offset);
                  p = offset * (end - start) + start;
  
              }
  
              mSpline[0].getPos(p, mInterpolateData);
              if (mArcSpline != null) {
                  if (mInterpolateData.length > 0) {
                      mArcSpline.getPos(p, mInterpolateData);
                  }
              }
              mStartMotionPath.getBounds(mInterpolateVariables, mInterpolateData, bounds, i * 2);
          }
      }
  
      private float getPreCycleDistance() {
          int pointCount = 100;
          float[] points = new float[2];
          float sum = 0;
          float mils = 1.0f / (pointCount - 1);
          double x = 0, y = 0;
          for (int i = 0; i < pointCount; i++) {
              float position = i * mils;
  
              double p = position;
  
              Easing easing = mStartMotionPath.mKeyFrameEasing;
              float start = 0;
              float end = Float.NaN;
              for (MotionPaths frame : mMotionPaths) {
                  if (frame.mKeyFrameEasing != null) { // this frame has an easing
                      if (frame.mTime < position) {  // frame with easing is before the current pos
                          easing = frame.mKeyFrameEasing; // this is the candidate
                          start = frame.mTime; // this is also the starting time
                      } else { // frame with easing is past the pos
                          if (Float.isNaN(end)) { // we never ended the time line
                              end = frame.mTime;
                          }
                      }
                  }
              }
  
              if (easing != null) {
                  if (Float.isNaN(end)) {
                      end = 1.0f;
                  }
                  float offset = (position - start) / (end - start);
                  offset = (float) easing.get(offset);
                  p = offset * (end - start) + start;
  
              }
  
              mSpline[0].getPos(p, mInterpolateData);
              mStartMotionPath.getCenter(p, mInterpolateVariables, mInterpolateData, points, 0);
              if (i > 0) {
                  sum += (float) Math.hypot(y - points[1], x - points[0]);
              }
              x = points[0];
              y = points[1];
          }
          return sum;
      }
  
      MotionKeyPosition getPositionKeyframe(int layoutWidth, int layoutHeight, float x, float y) {
          FloatRect start = new FloatRect();
          start.left = mStartMotionPath.mX;
          start.top = mStartMotionPath.mY;
          start.right = start.left + mStartMotionPath.mWidth;
          start.bottom = start.top + mStartMotionPath.mHeight;
          FloatRect end = new FloatRect();
          end.left = mEndMotionPath.mX;
          end.top = mEndMotionPath.mY;
          end.right = end.left + mEndMotionPath.mWidth;
          end.bottom = end.top + mEndMotionPath.mHeight;
          for (MotionKey key : mKeyList) {
              if (key instanceof MotionKeyPosition) {
                  if (((MotionKeyPosition) key).intersects(layoutWidth,
                          layoutHeight, start, end, x, y)) {
                      return (MotionKeyPosition) key;
                  }
              }
          }
          return null;
      }
  
      // @TODO: add description
      public int buildKeyFrames(float[] keyFrames, int[] mode, int[] pos) {
          if (keyFrames != null) {
              int count = 0;
              double[] time = mSpline[0].getTimePoints();
              if (mode != null) {
                  for (MotionPaths keyFrame : mMotionPaths) {
                      mode[count++] = keyFrame.mMode;
                  }
                  count = 0;
              }
              if (pos != null) {
                  for (MotionPaths keyFrame : mMotionPaths) {
                      pos[count++] = (int) (100 * keyFrame.mPosition);
                  }
                  count = 0;
              }
              for (int i = 0; i < time.length; i++) {
                  mSpline[0].getPos(time[i], mInterpolateData);
                  mStartMotionPath.getCenter(time[i],
                          mInterpolateVariables, mInterpolateData, keyFrames, count);
                  count += 2;
              }
              return count / 2;
          }
          return 0;
      }
  
      int buildKeyBounds(float[] keyBounds, int[] mode) {
          if (keyBounds != null) {
              int count = 0;
              double[] time = mSpline[0].getTimePoints();
              if (mode != null) {
                  for (MotionPaths keyFrame : mMotionPaths) {
                      mode[count++] = keyFrame.mMode;
                  }
                  count = 0;
              }
  
              for (int i = 0; i < time.length; i++) {
                  mSpline[0].getPos(time[i], mInterpolateData);
                  mStartMotionPath.getBounds(mInterpolateVariables,
                          mInterpolateData, keyBounds, count);
                  count += 2;
              }
              return count / 2;
          }
          return 0;
      }
  
      String[] mAttributeTable;
  
      int getAttributeValues(String attributeType, float[] points, int pointCount) {
          @SuppressWarnings("unused") float mils = 1.0f / (pointCount - 1);
          SplineSet spline = mAttributesMap.get(attributeType);
          if (spline == null) {
              return -1;
          }
          for (int j = 0; j < points.length; j++) {
              points[j] = spline.get(j / (points.length - 1));
          }
          return points.length;
      }
  
      // @TODO: add description
      public void buildRect(float p, float[] path, int offset) {
          p = getAdjustedPosition(p, null);
          mSpline[0].getPos(p, mInterpolateData);
          mStartMotionPath.getRect(mInterpolateVariables, mInterpolateData, path, offset);
      }
  
      void buildRectangles(float[] path, int pointCount) {
          float mils = 1.0f / (pointCount - 1);
          for (int i = 0; i < pointCount; i++) {
              float position = i * mils;
              position = getAdjustedPosition(position, null);
              mSpline[0].getPos(position, mInterpolateData);
              mStartMotionPath.getRect(mInterpolateVariables, mInterpolateData, path, i * 8);
          }
      }
  
      float getKeyFrameParameter(int type, float x, float y) {
  
          float dx = mEndMotionPath.mX - mStartMotionPath.mX;
          float dy = mEndMotionPath.mY - mStartMotionPath.mY;
          float startCenterX = mStartMotionPath.mX + mStartMotionPath.mWidth / 2;
          float startCenterY = mStartMotionPath.mY + mStartMotionPath.mHeight / 2;
          float hypotenuse = (float) Math.hypot(dx, dy);
          if (hypotenuse < 0.0000001) {
              return Float.NaN;
          }
  
          float vx = x - startCenterX;
          float vy = y - startCenterY;
          float distFromStart = (float) Math.hypot(vx, vy);
          if (distFromStart == 0) {
              return 0;
          }
          float pathDistance = (vx * dx + vy * dy);
  
          switch (type) {
              case PATH_PERCENT:
                  return pathDistance / hypotenuse;
              case PATH_PERPENDICULAR:
                  return (float) Math.sqrt(hypotenuse * hypotenuse - pathDistance * pathDistance);
              case HORIZONTAL_PATH_X:
                  return vx / dx;
              case HORIZONTAL_PATH_Y:
                  return vy / dx;
              case VERTICAL_PATH_X:
                  return vx / dy;
              case VERTICAL_PATH_Y:
                  return vy / dy;
          }
          return 0;
      }
  
      private void insertKey(MotionPaths point) {
          MotionPaths redundant = null;
          for (MotionPaths p : mMotionPaths) {
              if (point.mPosition == p.mPosition) {
                  redundant = p;
              }
          }
          if (redundant != null) {
              mMotionPaths.remove(redundant);
          }
          int pos = Collections.binarySearch(mMotionPaths, point);
          if (pos == 0) {
              Utils.loge(TAG, " KeyPath position \"" + point.mPosition + "\" outside of range");
          }
          mMotionPaths.add(-pos - 1, point);
      }
  
      void addKeys(ArrayList<MotionKey> list) {
          mKeyList.addAll(list);
          if (DEBUG) {
              for (MotionKey key : mKeyList) {
                  Utils.log(TAG, " ################ set = " + key.getClass().getSimpleName());
              }
          }
      }
  
      // @TODO: add description
      public void addKey(MotionKey key) {
          mKeyList.add(key);
          if (DEBUG) {
              Utils.log(TAG, " ################ addKey = " + key.getClass().getSimpleName());
          }
      }
  
      public void setPathMotionArc(int arc) {
          mPathMotionArc = arc;
      }
  
      /**
       * Called after all TimePoints & Cycles have been added;
       * Spines are evaluated
       */
      public void setup(int parentWidth,
              int parentHeight,
              float transitionDuration,
              long currentTime) {
          @SuppressWarnings({"unused", "ModifiedButNotUsed"})
          HashSet<String> springAttributes = new HashSet<>();
          // attributes we need to interpolate
          HashSet<String> timeCycleAttributes = new HashSet<>(); // attributes we need to interpolate
          HashSet<String> splineAttributes = new HashSet<>(); // attributes we need to interpolate
          HashSet<String> cycleAttributes = new HashSet<>(); // attributes we need to oscillate
          HashMap<String, Integer> interpolation = new HashMap<>();
          ArrayList<MotionKeyTrigger> triggerList = null;
  
          setupRelative();
          if (DEBUG) {
              if (mKeyList == null) {
                  Utils.log(TAG, ">>>>>>>>>>>>>>> mKeyList==null");
  
              } else {
                  Utils.log(TAG, ">>>>>>>>>>>>>>> mKeyList for " + mView.getName());
  
              }
          }
  
          if (mPathMotionArc != UNSET && mStartMotionPath.mPathMotionArc == UNSET) {
              mStartMotionPath.mPathMotionArc = mPathMotionArc;
          }
  
          mStartPoint.different(mEndPoint, splineAttributes);
          if (DEBUG) {
              HashSet<String> attr = new HashSet<>();
              mStartPoint.different(mEndPoint, attr);
              Utils.log(TAG, ">>>>>>>>>>>>>>> MotionConstrainedPoint found "
                      + Arrays.toString(attr.toArray()));
          }
          if (mKeyList != null) {
              for (MotionKey key : mKeyList) {
                  if (key instanceof MotionKeyPosition) {
                      MotionKeyPosition keyPath = (MotionKeyPosition) key;
                      insertKey(new MotionPaths(parentWidth, parentHeight,
                              keyPath, mStartMotionPath, mEndMotionPath));
                      if (keyPath.mCurveFit != UNSET) {
                          mCurveFitType = keyPath.mCurveFit;
                      }
                  } else if (key instanceof MotionKeyCycle) {
                      key.getAttributeNames(cycleAttributes);
                  } else if (key instanceof MotionKeyTimeCycle) {
                      key.getAttributeNames(timeCycleAttributes);
                  } else if (key instanceof MotionKeyTrigger) {
                      if (triggerList == null) {
                          triggerList = new ArrayList<>();
                      }
                      triggerList.add((MotionKeyTrigger) key);
                  } else {
                      key.setInterpolation(interpolation);
                      key.getAttributeNames(splineAttributes);
                  }
              }
          }
  
          //--------------------------- trigger support --------------------
  
          if (triggerList != null) {
              mKeyTriggers = triggerList.toArray(new MotionKeyTrigger[0]);
          }
  
          //--------------------------- splines support --------------------
          if (!splineAttributes.isEmpty()) {
              mAttributesMap = new HashMap<>();
              for (String attribute : splineAttributes) {
                  SplineSet splineSets;
                  if (attribute.startsWith("CUSTOM,")) {
                      KeyFrameArray.CustomVar attrList = new KeyFrameArray.CustomVar();
                      String customAttributeName = attribute.split(",")[1];
                      for (MotionKey key : mKeyList) {
                          if (key.mCustom == null) {
                              continue;
                          }
                          CustomVariable customAttribute = key.mCustom.get(customAttributeName);
                          if (customAttribute != null) {
                              attrList.append(key.mFramePosition, customAttribute);
                          }
                      }
                      splineSets = SplineSet.makeCustomSplineSet(attribute, attrList);
                  } else {
                      splineSets = SplineSet.makeSpline(attribute, currentTime);
                  }
                  if (splineSets == null) {
                      continue;
                  }
                  splineSets.setType(attribute);
                  mAttributesMap.put(attribute, splineSets);
              }
              if (mKeyList != null) {
                  for (MotionKey key : mKeyList) {
                      if ((key instanceof MotionKeyAttributes)) {
                          key.addValues(mAttributesMap);
                      }
                  }
              }
              mStartPoint.addValues(mAttributesMap, 0);
              mEndPoint.addValues(mAttributesMap, 100);
  
              for (String spline : mAttributesMap.keySet()) {
                  int curve = CurveFit.SPLINE; // default is SPLINE
                  if (interpolation.containsKey(spline)) {
                      Integer boxedCurve = interpolation.get(spline);
                      if (boxedCurve != null) {
                          curve = boxedCurve;
                      }
                  }
                  SplineSet splineSet = mAttributesMap.get(spline);
                  if (splineSet != null) {
                      splineSet.setup(curve);
                  }
              }
          }
  
          //--------------------------- timeCycle support --------------------
          if (!timeCycleAttributes.isEmpty()) {
              if (mTimeCycleAttributesMap == null) {
                  mTimeCycleAttributesMap = new HashMap<>();
              }
              for (String attribute : timeCycleAttributes) {
                  if (mTimeCycleAttributesMap.containsKey(attribute)) {
                      continue;
                  }
  
                  SplineSet splineSets = null;
                  if (attribute.startsWith("CUSTOM,")) {
                      KeyFrameArray.CustomVar attrList = new KeyFrameArray.CustomVar();
                      String customAttributeName = attribute.split(",")[1];
                      for (MotionKey key : mKeyList) {
                          if (key.mCustom == null) {
                              continue;
                          }
                          CustomVariable customAttribute = key.mCustom.get(customAttributeName);
                          if (customAttribute != null) {
                              attrList.append(key.mFramePosition, customAttribute);
                          }
                      }
                      splineSets = SplineSet.makeCustomSplineSet(attribute, attrList);
                  } else {
                      splineSets = SplineSet.makeSpline(attribute, currentTime);
                  }
                  if (splineSets == null) {
                      continue;
                  }
                  splineSets.setType(attribute);
  //                mTimeCycleAttributesMap.put(attribute, splineSets);
              }
  
              if (mKeyList != null) {
                  for (MotionKey key : mKeyList) {
                      if (key instanceof MotionKeyTimeCycle) {
                          ((MotionKeyTimeCycle) key).addTimeValues(mTimeCycleAttributesMap);
                      }
                  }
              }
  
              for (String spline : mTimeCycleAttributesMap.keySet()) {
                  int curve = CurveFit.SPLINE; // default is SPLINE
                  if (interpolation.containsKey(spline)) {
                      curve = interpolation.get(spline);
                  }
                  mTimeCycleAttributesMap.get(spline).setup(curve);
              }
          }
  
          //--------------------------------- end new key frame 2
  
          MotionPaths[] points = new MotionPaths[2 + mMotionPaths.size()];
          int count = 1;
          points[0] = mStartMotionPath;
          points[points.length - 1] = mEndMotionPath;
          if (mMotionPaths.size() > 0 && mCurveFitType == MotionKey.UNSET) {
              mCurveFitType = CurveFit.SPLINE;
          }
          for (MotionPaths point : mMotionPaths) {
              points[count++] = point;
          }
  
          // -----  setup custom attributes which must be in the start and end constraint sets
          int variables = 18;
          HashSet<String> attributeNameSet = new HashSet<>();
          for (String s : mEndMotionPath.mCustomAttributes.keySet()) {
              if (mStartMotionPath.mCustomAttributes.containsKey(s)) {
                  if (!splineAttributes.contains("CUSTOM," + s)) {
                      attributeNameSet.add(s);
                  }
              }
          }
  
          mAttributeNames = attributeNameSet.toArray(new String[0]);
          mAttributeInterpolatorCount = new int[mAttributeNames.length];
          for (int i = 0; i < mAttributeNames.length; i++) {
              String attributeName = mAttributeNames[i];
              mAttributeInterpolatorCount[i] = 0;
              for (int j = 0; j < points.length; j++) {
                  if (points[j].mCustomAttributes.containsKey(attributeName)) {
                      CustomVariable attribute = points[j].mCustomAttributes.get(attributeName);
                      if (attribute != null) {
                          mAttributeInterpolatorCount[i] += attribute.numberOfInterpolatedValues();
                          break;
                      }
                  }
              }
          }
          boolean arcMode = points[0].mPathMotionArc != UNSET;
          boolean[] mask = new boolean[variables + mAttributeNames.length]; // defaults to false
          for (int i = 1; i < points.length; i++) {
              points[i].different(points[i - 1], mask, mAttributeNames, arcMode);
          }
  
          count = 0;
          for (int i = 1; i < mask.length; i++) {
              if (mask[i]) {
                  count++;
              }
          }
  
          mInterpolateVariables = new int[count];
          int varLen = Math.max(2, count);
          mInterpolateData = new double[varLen];
          mInterpolateVelocity = new double[varLen];
  
          count = 0;
          for (int i = 1; i < mask.length; i++) {
              if (mask[i]) {
                  mInterpolateVariables[count++] = i;
              }
          }
  
          double[][] splineData = new double[points.length][mInterpolateVariables.length];
          double[] timePoint = new double[points.length];
  
          for (int i = 0; i < points.length; i++) {
              points[i].fillStandard(splineData[i], mInterpolateVariables);
              timePoint[i] = points[i].mTime;
          }
  
          for (int j = 0; j < mInterpolateVariables.length; j++) {
              int interpolateVariable = mInterpolateVariables[j];
              if (interpolateVariable < MotionPaths.sNames.length) {
                  @SuppressWarnings("unused") String s =
                          MotionPaths.sNames[mInterpolateVariables[j]] + " [";
                  for (int i = 0; i < points.length; i++) {
                      s += splineData[i][j];
                  }
              }
          }
          mSpline = new CurveFit[1 + mAttributeNames.length];
  
          for (int i = 0; i < mAttributeNames.length; i++) {
              int pointCount = 0;
              double[][] splinePoints = null;
              double[] timePoints = null;
              String name = mAttributeNames[i];
  
              for (int j = 0; j < points.length; j++) {
                  if (points[j].hasCustomData(name)) {
                      if (splinePoints == null) {
                          timePoints = new double[points.length];
                          splinePoints =
                                  new double[points.length][points[j].getCustomDataCount(name)];
                      }
                      timePoints[pointCount] = points[j].mTime;
                      points[j].getCustomData(name, splinePoints[pointCount], 0);
                      pointCount++;
                  }
              }
              timePoints = Arrays.copyOf(timePoints, pointCount);
              splinePoints = Arrays.copyOf(splinePoints, pointCount);
              mSpline[i + 1] = CurveFit.get(mCurveFitType, timePoints, splinePoints);
          }
  
          // Spline for positions
          mSpline[0] = CurveFit.get(mCurveFitType, timePoint, splineData);
          // --------------------------- SUPPORT ARC MODE --------------
          if (points[0].mPathMotionArc != UNSET) {
              int size = points.length;
              int[] mode = new int[size];
              double[] time = new double[size];
              double[][] values = new double[size][2];
              for (int i = 0; i < size; i++) {
                  mode[i] = points[i].mPathMotionArc;
                  time[i] = points[i].mTime;
                  values[i][0] = points[i].mX;
                  values[i][1] = points[i].mY;
              }
  
              mArcSpline = CurveFit.getArc(mode, time, values);
          }
  
          //--------------------------- Cycle support --------------------
          float distance = Float.NaN;
          mCycleMap = new HashMap<>();
          if (mKeyList != null) {
              for (String attribute : cycleAttributes) {
                  KeyCycleOscillator cycle = KeyCycleOscillator.makeWidgetCycle(attribute);
                  if (cycle == null) {
                      continue;
                  }
  
                  if (cycle.variesByPath()) {
                      if (Float.isNaN(distance)) {
                          distance = getPreCycleDistance();
                      }
                  }
                  cycle.setType(attribute);
                  mCycleMap.put(attribute, cycle);
              }
              for (MotionKey key : mKeyList) {
                  if (key instanceof MotionKeyCycle) {
                      ((MotionKeyCycle) key).addCycleValues(mCycleMap);
                  }
              }
              for (KeyCycleOscillator cycle : mCycleMap.values()) {
                  cycle.setup(distance);
              }
          }
  
          if (DEBUG) {
              Utils.log(TAG, "Animation of splineAttributes "
                      + Arrays.toString(splineAttributes.toArray()));
              Utils.log(TAG, "Animation of cycle " + Arrays.toString(mCycleMap.keySet().toArray()));
              if (mAttributesMap != null) {
                  Utils.log(TAG, " splines = " + Arrays.toString(mAttributesMap.keySet().toArray()));
                  for (String s : mAttributesMap.keySet()) {
                      Utils.log(TAG, s + " = " + mAttributesMap.get(s));
                  }
              }
              Utils.log(TAG, " ---------------------------------------- ");
          }
  
          //--------------------------- end cycle support ----------------
      }
  
      /**
       * Debug string
       */
      @Override
      public String toString() {
          return " start: x: " + mStartMotionPath.mX + " y: " + mStartMotionPath.mY
                  + " end: x: " + mEndMotionPath.mX + " y: " + mEndMotionPath.mY;
      }
  
      private void readView(MotionPaths motionPaths) {
          motionPaths.setBounds((int) mView.getX(), (int) mView.getY(),
                  mView.getWidth(), mView.getHeight());
      }
  
      public void setView(MotionWidget view) {
          mView = view;
      }
  
      public MotionWidget getView() {
          return mView;
      }
  
      // @TODO: add description
      public void setStart(MotionWidget mw) {
          mStartMotionPath.mTime = 0;
          mStartMotionPath.mPosition = 0;
          mStartMotionPath.setBounds(mw.getX(), mw.getY(), mw.getWidth(), mw.getHeight());
          mStartMotionPath.applyParameters(mw);
          mStartPoint.setState(mw);
          TypedBundle p = mw.getWidgetFrame().getMotionProperties();
          if (p != null) {
              p.applyDelta(this);
          }
      }
  
      // @TODO: add description
      public void setEnd(MotionWidget mw) {
          mEndMotionPath.mTime = 1;
          mEndMotionPath.mPosition = 1;
          readView(mEndMotionPath);
          mEndMotionPath.setBounds(mw.getLeft(), mw.getTop(), mw.getWidth(), mw.getHeight());
          mEndMotionPath.applyParameters(mw);
          mEndPoint.setState(mw);
      }
  
      // @TODO: add description
      public void setStartState(ViewState rect,
              MotionWidget v,
              int rotation,
              int preWidth,
              int preHeight) {
          mStartMotionPath.mTime = 0;
          mStartMotionPath.mPosition = 0;
          int cx, cy;
          Rect r = new Rect();
          switch (rotation) {
              case 2:
                  cx = rect.left + rect.right;
                  cy = rect.top + rect.bottom;
                  r.left = preHeight - (cy + rect.width()) / 2;
                  r.top = (cx - rect.height()) / 2;
                  r.right = r.left + rect.width();
                  r.bottom = r.top + rect.height();
                  break;
              case 1:
                  cx = rect.left + rect.right;
                  cy = rect.top + rect.bottom;
                  r.left = (cy - rect.width()) / 2;
                  r.top = preWidth - (cx + rect.height()) / 2;
                  r.right = r.left + rect.width();
                  r.bottom = r.top + rect.height();
                  break;
          }
          mStartMotionPath.setBounds(r.left, r.top, r.width(), r.height());
          mStartPoint.setState(r, v, rotation, rect.rotation);
      }
  
      void rotate(Rect rect, Rect out, int rotation, int preHeight, int preWidth) {
          int cx, cy;
          switch (rotation) {
  
              case MotionConstraintSet.ROTATE_PORTRATE_OF_LEFT:
                  cx = rect.left + rect.right;
                  cy = rect.top + rect.bottom;
                  out.left = preHeight - (cy + rect.width()) / 2;
                  out.top = (cx - rect.height()) / 2;
                  out.right = out.left + rect.width();
                  out.bottom = out.top + rect.height();
                  break;
              case MotionConstraintSet.ROTATE_PORTRATE_OF_RIGHT:
                  cx = rect.left + rect.right;
                  cy = rect.top + rect.bottom;
                  out.left = (cy - rect.width()) / 2;
                  out.top = preWidth - (cx + rect.height()) / 2;
                  out.right = out.left + rect.width();
                  out.bottom = out.top + rect.height();
                  break;
              case MotionConstraintSet.ROTATE_LEFT_OF_PORTRATE:
                  cx = rect.left + rect.right;
                  cy = rect.bottom + rect.top;
                  out.left = preHeight - (cy + rect.width()) / 2;
                  out.top = (cx - rect.height()) / 2;
                  out.right = out.left + rect.width();
                  out.bottom = out.top + rect.height();
                  break;
              case MotionConstraintSet.ROTATE_RIGHT_OF_PORTRATE:
                  cx = rect.left + rect.right;
                  cy = rect.top + rect.bottom;
                  out.left = rect.height() / 2 + rect.top - cx / 2;
                  out.top = preWidth - (cx + rect.height()) / 2;
                  out.right = out.left + rect.width();
                  out.bottom = out.top + rect.height();
                  break;
          }
      }
  
      // Todo : Implement  QuantizeMotion scene rotate
      //    void setStartState(Rect cw, ConstraintSet constraintSet,
      //                      int parentWidth, int parentHeight) {
      //        int rotate = constraintSet.mRotate; // for rotated frames
      //        if (rotate != 0) {
      //            rotate(cw, mTempRect, rotate, parentWidth, parentHeight);
      //        }
      //        mStartMotionPath.time = 0;
      //        mStartMotionPath.position = 0;
      //        readView(mStartMotionPath);
      //        mStartMotionPath.setBounds(cw.left, cw.top, cw.width(), cw.height());
      //        ConstraintSet.Constraint constraint = constraintSet.getParameters(mId);
      //        mStartMotionPath.applyParameters(constraint);
      //        mMotionStagger = constraint.motion.mMotionStagger;
      //        mStartPoint.setState(cw, constraintSet, rotate, mId);
      //        mTransformPivotTarget = constraint.transform.transformPivotTarget;
      //        mQuantizeMotionSteps = constraint.motion.mQuantizeMotionSteps;
      //        mQuantizeMotionPhase = constraint.motion.mQuantizeMotionPhase;
      //        mQuantizeMotionInterpolator = getInterpolator(mView.getContext(),
      //                constraint.motion.mQuantizeInterpolatorType,
      //                constraint.motion.mQuantizeInterpolatorString,
      //                constraint.motion.mQuantizeInterpolatorID
      //        );
      //    }
  
      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;
      private static final int SPLINE_STRING = -1;
      @SuppressWarnings("unused") private static final int INTERPOLATOR_REFERENCE_ID = -2;
      @SuppressWarnings("unused") private static final int INTERPOLATOR_UNDEFINED = -3;
  
      private static DifferentialInterpolator getInterpolator(int type,
              String interpolatorString,
              @SuppressWarnings("unused") int id) {
          switch (type) {
              case SPLINE_STRING:
                  final Easing easing = Easing.getInterpolator(interpolatorString);
                  return new DifferentialInterpolator() {
                      float mX;
  
                      @Override
                      public float getInterpolation(float x) {
                          mX = x;
                          return (float) easing.get(x);
                      }
  
                      @Override
                      public float getVelocity() {
                          return (float) easing.getDiff(mX);
                      }
                  };
  
          }
          return null;
      }
  
  //    void setEndState(Rect cw, ConstraintSet constraintSet, int parentWidth, int parentHeight) {
  //        int rotate = constraintSet.mRotate; // for rotated frames
  //        if (rotate != 0) {
  //            rotate(cw, mTempRect, rotate, parentWidth, parentHeight);
  //            cw = mTempRect;
  //        }
  //        mEndMotionPath.time = 1;
  //        mEndMotionPath.position = 1;
  //        readView(mEndMotionPath);
  //        mEndMotionPath.setBounds(cw.left, cw.top, cw.width(), cw.height());
  //        mEndMotionPath.applyParameters(constraintSet.getParameters(mId));
  //        mEndPoint.setState(cw, constraintSet, rotate, mId);
  //    }
  
      void setBothStates(MotionWidget v) {
          mStartMotionPath.mTime = 0;
          mStartMotionPath.mPosition = 0;
          mNoMovement = true;
          mStartMotionPath.setBounds(v.getX(), v.getY(), v.getWidth(), v.getHeight());
          mEndMotionPath.setBounds(v.getX(), v.getY(), v.getWidth(), v.getHeight());
          mStartPoint.setState(v);
          mEndPoint.setState(v);
      }
  
      /**
       * Calculates the adjusted position (and optional velocity).
       * Note if requesting velocity staggering is not considered
       *
       * @param position position pre stagger
       * @param velocity return velocity
       * @return actual position accounting for easing and staggering
       */
      private float getAdjustedPosition(float position, float[] velocity) {
          if (velocity != null) {
              velocity[0] = 1;
          } else if (mStaggerScale != 1.0) {
              if (position < mStaggerOffset) {
                  position = 0;
              }
              if (position > mStaggerOffset && position < 1.0) {
                  position -= mStaggerOffset;
                  position *= mStaggerScale;
                  position = Math.min(position, 1.0f);
              }
          }
  
          // adjust the position based on the easing curve
          float adjusted = position;
          Easing easing = mStartMotionPath.mKeyFrameEasing;
          float start = 0;
          float end = Float.NaN;
          for (MotionPaths frame : mMotionPaths) {
              if (frame.mKeyFrameEasing != null) { // this frame has an easing
                  if (frame.mTime < position) {  // frame with easing is before the current pos
                      easing = frame.mKeyFrameEasing; // this is the candidate
                      start = frame.mTime; // this is also the starting time
                  } else { // frame with easing is past the pos
                      if (Float.isNaN(end)) { // we never ended the time line
                          end = frame.mTime;
                      }
                  }
              }
          }
  
          if (easing != null) {
              if (Float.isNaN(end)) {
                  end = 1.0f;
              }
              float offset = (position - start) / (end - start);
              float new_offset = (float) easing.get(offset);
              adjusted = new_offset * (end - start) + start;
              if (velocity != null) {
                  velocity[0] = (float) easing.getDiff(offset);
              }
          }
          return adjusted;
      }
  
      void endTrigger(boolean start) {
  //        if ("button".equals(Debug.getName(mView)))
  //            if (mKeyTriggers != null) {
  //                for (int i = 0; i < mKeyTriggers.length; i++) {
  //                    mKeyTriggers[i].conditionallyFire(start ? -100 : 100, mView);
  //                }
  //            }
      }
      //##############################################################################################
      //$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%
      //$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%
      //$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%
      //$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%$%
      //##############################################################################################
  
      /**
       * The main driver of interpolation
       *
       * @return do you need to keep animating
       */
      public boolean interpolate(MotionWidget child,
              float globalPosition,
              long time,
              KeyCache keyCache) {
          @SuppressWarnings("unused")  boolean timeAnimation = false;
          float position = getAdjustedPosition(globalPosition, null);
          // This quantize the position into steps e.g. 4 steps = 0-0.25,0.25-0.50 etc
          if (mQuantizeMotionSteps != UNSET) {
              @SuppressWarnings("unused") float pin = position;
              float steps = 1.0f / mQuantizeMotionSteps; // the length of a step
              float jump = (float) Math.floor(position / steps) * steps; // step jumps
              float section = (position % steps) / steps; // float from 0 to 1 in a step
  
              if (!Float.isNaN(mQuantizeMotionPhase)) {
                  section = (section + mQuantizeMotionPhase) % 1;
              }
              if (mQuantizeMotionInterpolator != null) {
                  section = mQuantizeMotionInterpolator.getInterpolation(section);
              } else {
                  section = section > 0.5 ? 1 : 0;
              }
              position = section * steps + jump;
          }
          // MotionKeyTimeCycle.PathRotate timePathRotate = null;
          if (mAttributesMap != null) {
              for (SplineSet aSpline : mAttributesMap.values()) {
                  aSpline.setProperty(child, position);
              }
          }
  
          //       TODO add KeyTimeCycle
          //        if (mTimeCycleAttributesMap != null) {
          //            for (ViewTimeCycle aSpline : mTimeCycleAttributesMap.values()) {
          //                if (aSpline instanceof ViewTimeCycle.PathRotate) {
          //                    timePathRotate = (ViewTimeCycle.PathRotate) aSpline;
          //                    continue;
          //                }
          //                timeAnimation |= aSpline.setProperty(child, position, time, keyCache);
          //            }
          //        }
  
          if (mSpline != null) {
              mSpline[0].getPos(position, mInterpolateData);
              mSpline[0].getSlope(position, mInterpolateVelocity);
              if (mArcSpline != null) {
                  if (mInterpolateData.length > 0) {
                      mArcSpline.getPos(position, mInterpolateData);
                      mArcSpline.getSlope(position, mInterpolateVelocity);
                  }
              }
  
              if (!mNoMovement) {
                  mStartMotionPath.setView(position, child,
                          mInterpolateVariables, mInterpolateData, mInterpolateVelocity, null);
              }
              if (mTransformPivotTarget != UNSET) {
                  if (mTransformPivotView == null) {
                      MotionWidget layout = (MotionWidget) child.getParent();
                      mTransformPivotView = layout.findViewById(mTransformPivotTarget);
                  }
                  if (mTransformPivotView != null) {
                      float cy =
                              (mTransformPivotView.getTop() + mTransformPivotView.getBottom()) / 2.0f;
                      float cx =
                              (mTransformPivotView.getLeft() + mTransformPivotView.getRight()) / 2.0f;
                      if (child.getRight() - child.getLeft() > 0
                              && child.getBottom() - child.getTop() > 0) {
                          float px = (cx - child.getLeft());
                          float py = (cy - child.getTop());
                          child.setPivotX(px);
                          child.setPivotY(py);
                      }
                  }
              }
  
              //       TODO add support for path rotate
              //            if (mAttributesMap != null) {
              //                for (SplineSet aSpline : mAttributesMap.values()) {
              //                    if (aSpline instanceof ViewSpline.PathRotate
              //                          && mInterpolateVelocity.length > 1)
              //                        ((ViewSpline.PathRotate) aSpline).setPathRotate(child,
              //                        position, mInterpolateVelocity[0], mInterpolateVelocity[1]);
              //                }
              //
              //            }
              //            if (timePathRotate != null) {
              //                timeAnimation |= timePathRotate.setPathRotate(child, keyCache,
              //                  position, time, mInterpolateVelocity[0], mInterpolateVelocity[1]);
              //            }
  
              for (int i = 1; i < mSpline.length; i++) {
                  CurveFit spline = mSpline[i];
                  spline.getPos(position, mValuesBuff);
                  //interpolated here
                  mStartMotionPath.mCustomAttributes
                          .get(mAttributeNames[i - 1])
                          .setInterpolatedValue(child, mValuesBuff);
              }
              if (mStartPoint.mVisibilityMode == MotionWidget.VISIBILITY_MODE_NORMAL) {
                  if (position <= 0.0f) {
                      child.setVisibility(mStartPoint.mVisibility);
                  } else if (position >= 1.0f) {
                      child.setVisibility(mEndPoint.mVisibility);
                  } else if (mEndPoint.mVisibility != mStartPoint.mVisibility) {
                      child.setVisibility(MotionWidget.VISIBLE);
                  }
              }
  
              if (mKeyTriggers != null) {
                  for (int i = 0; i < mKeyTriggers.length; i++) {
                      mKeyTriggers[i].conditionallyFire(position, child);
                  }
              }
          } else {
              // do the interpolation
  
              float float_l =
                      (mStartMotionPath.mX + (mEndMotionPath.mX - mStartMotionPath.mX) * position);
              float float_t =
                      (mStartMotionPath.mY + (mEndMotionPath.mY - mStartMotionPath.mY) * position);
              float float_width = (mStartMotionPath.mWidth
                      + (mEndMotionPath.mWidth - mStartMotionPath.mWidth) * position);
              float float_height = (mStartMotionPath.mHeight
                      + (mEndMotionPath.mHeight - mStartMotionPath.mHeight) * position);
              int l = (int) (0.5f + float_l);
              int t = (int) (0.5f + float_t);
              int r = (int) (0.5f + float_l + float_width);
              int b = (int) (0.5f + float_t + float_height);
              int width = r - l;
              int height = b - t;
  
              if (FAVOR_FIXED_SIZE_VIEWS) {
                  l = (int) (mStartMotionPath.mX
                          + (mEndMotionPath.mX - mStartMotionPath.mX) * position);
                  t = (int) (mStartMotionPath.mY
                          + (mEndMotionPath.mY - mStartMotionPath.mY) * position);
                  width = (int) (mStartMotionPath.mWidth
                          + (mEndMotionPath.mWidth - mStartMotionPath.mWidth) * position);
                  height = (int) (mStartMotionPath.mHeight
                          + (mEndMotionPath.mHeight - mStartMotionPath.mHeight) * position);
                  r = l + width;
                  b = t + height;
              }
              // widget is responsible to call measure
              child.layout(l, t, r, b);
          }
  
          // TODO add pathRotate KeyCycles
          if (mCycleMap != null) {
              for (KeyCycleOscillator osc : mCycleMap.values()) {
                  if (osc instanceof KeyCycleOscillator.PathRotateSet) {
                      ((KeyCycleOscillator.PathRotateSet) osc).setPathRotate(child, position,
                              mInterpolateVelocity[0], mInterpolateVelocity[1]);
                  } else {
                      osc.setProperty(child, position);
                  }
              }
          }
          //   When we support TimeCycle return true if repaint is needed
          //        return timeAnimation;
          return false;
      }
  
      /**
       * This returns the differential with respect to the animation layout position (Progress)
       * of a point on the view (post layout effects are not computed)
       *
       * @param position    position in time
       * @param locationX   the x location on the view (0 = left edge, 1 = right edge)
       * @param locationY   the y location on the view (0 = top, 1 = bottom)
       * @param mAnchorDpDt returns the differential of the motion with respect to the position
       */
      public void getDpDt(float position, float locationX, float locationY, float[] mAnchorDpDt) {
          position = getAdjustedPosition(position, mVelocity);
  
          if (mSpline != null) {
              mSpline[0].getSlope(position, mInterpolateVelocity);
              mSpline[0].getPos(position, mInterpolateData);
              float v = mVelocity[0];
              for (int i = 0; i < mInterpolateVelocity.length; i++) {
                  mInterpolateVelocity[i] *= v;
              }
  
              if (mArcSpline != null) {
                  if (mInterpolateData.length > 0) {
                      mArcSpline.getPos(position, mInterpolateData);
                      mArcSpline.getSlope(position, mInterpolateVelocity);
                      mStartMotionPath.setDpDt(locationX, locationY, mAnchorDpDt,
                              mInterpolateVariables, mInterpolateVelocity, mInterpolateData);
                  }
                  return;
              }
              mStartMotionPath.setDpDt(locationX, locationY, mAnchorDpDt,
                      mInterpolateVariables, mInterpolateVelocity, mInterpolateData);
              return;
          }
          // do the interpolation
          float dleft = (mEndMotionPath.mX - mStartMotionPath.mX);
          float dTop = (mEndMotionPath.mY - mStartMotionPath.mY);
          float dWidth = (mEndMotionPath.mWidth - mStartMotionPath.mWidth);
          float dHeight = (mEndMotionPath.mHeight - mStartMotionPath.mHeight);
          float dRight = dleft + dWidth;
          float dBottom = dTop + dHeight;
          mAnchorDpDt[0] = dleft * (1 - locationX) + dRight * locationX;
          mAnchorDpDt[1] = dTop * (1 - locationY) + dBottom * locationY;
      }
  
      /**
       * This returns the differential with respect to the animation post layout transform
       * of a point on the view
       *
       * @param position    position in time
       * @param width       width of the view
       * @param height      height of the view
       * @param locationX   the x location on the view (0 = left edge, 1 = right edge)
       * @param locationY   the y location on the view (0 = top, 1 = bottom)
       * @param mAnchorDpDt returns the differential of the motion with respect to the position
       */
      void getPostLayoutDvDp(float position,
              int width,
              int height,
              float locationX,
              float locationY,
              float[] mAnchorDpDt) {
          if (DEBUG) {
              Utils.log(TAG, " position= " + position
                      + " location= " + locationX + " , " + locationY);
          }
          position = getAdjustedPosition(position, mVelocity);
  
          SplineSet trans_x =
                  (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.TRANSLATION_X);
          SplineSet trans_y =
                  (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.TRANSLATION_Y);
          SplineSet rotation =
                  (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.ROTATION);
          SplineSet scale_x = (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.SCALE_X);
          SplineSet scale_y = (mAttributesMap == null) ? null : mAttributesMap.get(MotionKey.SCALE_Y);
  
          KeyCycleOscillator osc_x =
                  (mCycleMap == null) ? null : mCycleMap.get(MotionKey.TRANSLATION_X);
          KeyCycleOscillator osc_y =
                  (mCycleMap == null) ? null : mCycleMap.get(MotionKey.TRANSLATION_Y);
          KeyCycleOscillator osc_r = (mCycleMap == null) ? null : mCycleMap.get(MotionKey.ROTATION);
          KeyCycleOscillator osc_sx = (mCycleMap == null) ? null : mCycleMap.get(MotionKey.SCALE_X);
          KeyCycleOscillator osc_sy = (mCycleMap == null) ? null : mCycleMap.get(MotionKey.SCALE_Y);
  
          VelocityMatrix vmat = new VelocityMatrix();
          vmat.clear();
          vmat.setRotationVelocity(rotation, position);
          vmat.setTranslationVelocity(trans_x, trans_y, position);
          vmat.setScaleVelocity(scale_x, scale_y, position);
          vmat.setRotationVelocity(osc_r, position);
          vmat.setTranslationVelocity(osc_x, osc_y, position);
          vmat.setScaleVelocity(osc_sx, osc_sy, position);
          if (mArcSpline != null) {
              if (mInterpolateData.length > 0) {
                  mArcSpline.getPos(position, mInterpolateData);
                  mArcSpline.getSlope(position, mInterpolateVelocity);
                  mStartMotionPath.setDpDt(locationX, locationY, mAnchorDpDt,
                          mInterpolateVariables, mInterpolateVelocity, mInterpolateData);
              }
              vmat.applyTransform(locationX, locationY, width, height, mAnchorDpDt);
              return;
          }
          if (mSpline != null) {
              position = getAdjustedPosition(position, mVelocity);
              mSpline[0].getSlope(position, mInterpolateVelocity);
              mSpline[0].getPos(position, mInterpolateData);
              float v = mVelocity[0];
              for (int i = 0; i < mInterpolateVelocity.length; i++) {
                  mInterpolateVelocity[i] *= v;
              }
              mStartMotionPath.setDpDt(locationX, locationY, mAnchorDpDt,
                      mInterpolateVariables, mInterpolateVelocity, mInterpolateData);
              vmat.applyTransform(locationX, locationY, width, height, mAnchorDpDt);
              return;
          }
  
          // do the interpolation
          float dleft = (mEndMotionPath.mX - mStartMotionPath.mX);
          float dTop = (mEndMotionPath.mY - mStartMotionPath.mY);
          float dWidth = (mEndMotionPath.mWidth - mStartMotionPath.mWidth);
          float dHeight = (mEndMotionPath.mHeight - mStartMotionPath.mHeight);
          float dRight = dleft + dWidth;
          float dBottom = dTop + dHeight;
          mAnchorDpDt[0] = dleft * (1 - locationX) + dRight * locationX;
          mAnchorDpDt[1] = dTop * (1 - locationY) + dBottom * locationY;
  
          vmat.clear();
          vmat.setRotationVelocity(rotation, position);
          vmat.setTranslationVelocity(trans_x, trans_y, position);
          vmat.setScaleVelocity(scale_x, scale_y, position);
          vmat.setRotationVelocity(osc_r, position);
          vmat.setTranslationVelocity(osc_x, osc_y, position);
          vmat.setScaleVelocity(osc_sx, osc_sy, position);
          vmat.applyTransform(locationX, locationY, width, height, mAnchorDpDt);
          return;
      }
  
      // @TODO: add description
      public int getDrawPath() {
          int mode = mStartMotionPath.mDrawPath;
          for (MotionPaths keyFrame : mMotionPaths) {
              mode = Math.max(mode, keyFrame.mDrawPath);
          }
          mode = Math.max(mode, mEndMotionPath.mDrawPath);
          return mode;
      }
  
      public void setDrawPath(int debugMode) {
          mStartMotionPath.mDrawPath = debugMode;
      }
  
      String name() {
  
          return mView.getName();
      }
  
      void positionKeyframe(MotionWidget view,
              MotionKeyPosition key,
              float x,
              float y,
              String[] attribute,
              float[] value) {
          FloatRect start = new FloatRect();
          start.left = mStartMotionPath.mX;
          start.top = mStartMotionPath.mY;
          start.right = start.left + mStartMotionPath.mWidth;
          start.bottom = start.top + mStartMotionPath.mHeight;
          FloatRect end = new FloatRect();
          end.left = mEndMotionPath.mX;
          end.top = mEndMotionPath.mY;
          end.right = end.left + mEndMotionPath.mWidth;
          end.bottom = end.top + mEndMotionPath.mHeight;
          key.positionAttributes(view, start, end, x, y, attribute, value);
      }
  
      /**
       * Get the keyFrames for the view controlled by this MotionController
       *
       * @param type is position(0-100) + 1000
       *             * mType(1=Attributes, 2=Position, 3=TimeCycle 4=Cycle 5=Trigger
       * @param pos  the x&y position of the keyFrame along the path
       * @return Number of keyFrames found
       */
      public int getKeyFramePositions(int[] type, float[] pos) {
          int i = 0;
          int count = 0;
          for (MotionKey key : mKeyList) {
              type[i++] = key.mFramePosition + 1000 * key.mType;
              float time = key.mFramePosition / 100.0f;
              mSpline[0].getPos(time, mInterpolateData);
              mStartMotionPath.getCenter(time, mInterpolateVariables, mInterpolateData, pos, count);
              count += 2;
          }
  
          return i;
      }
  
      /**
       * Gets the keyFrames for the view controlled by this MotionController.
       * The info data structure is of the form
       * 0 length if your are at index i the [i+len+1] is the next entry
       * 1 type  1=Attributes, 2=Position, 3=TimeCycle 4=Cycle 5=Trigger
       * 2 position
       * 3 x location
       * 4 y location
       * 5
       * ...
       * length
       *
       * @param info is a data structure array of int that holds info on each keyframe
       * @return Number of keyFrames found
       */
      public int getKeyFrameInfo(int type, int[] info) {
          int count = 0;
          int cursor = 0;
          float[] pos = new float[2];
          int len;
          for (MotionKey key : mKeyList) {
              if (key.mType != type && type == -1) {
                  continue;
              }
              len = cursor;
              info[cursor] = 0;
  
              info[++cursor] = key.mType;
              info[++cursor] = key.mFramePosition;
  
              float time = key.mFramePosition / 100.0f;
              mSpline[0].getPos(time, mInterpolateData);
              mStartMotionPath.getCenter(time, mInterpolateVariables, mInterpolateData, pos, 0);
              info[++cursor] = Float.floatToIntBits(pos[0]);
              info[++cursor] = Float.floatToIntBits(pos[1]);
              if (key instanceof MotionKeyPosition) {
                  MotionKeyPosition kp = (MotionKeyPosition) key;
                  info[++cursor] = kp.mPositionType;
  
                  info[++cursor] = Float.floatToIntBits(kp.mPercentX);
                  info[++cursor] = Float.floatToIntBits(kp.mPercentY);
              }
              cursor++;
              info[len] = cursor - len;
              count++;
          }
  
          return count;
      }
  
      @Override
      public boolean setValue(int id, int value) {
          switch (id) {
              case TypedValues.PositionType.TYPE_PATH_MOTION_ARC:
                  setPathMotionArc(value);
                  return true;
              case TypedValues.MotionType.TYPE_QUANTIZE_MOTIONSTEPS:
                  mQuantizeMotionSteps = value;
                  return true;
              case TypedValues.TransitionType.TYPE_AUTO_TRANSITION:
                  // TODO add support for auto transitions mAutoTransition = value;
                  return true;
          }
          return false;
      }
  
      @Override
      public boolean setValue(int id, float value) {
          if (MotionType.TYPE_QUANTIZE_MOTION_PHASE == id) {
              mQuantizeMotionPhase = value;
              return true;
          }
          if (MotionType.TYPE_STAGGER == id) {
              mMotionStagger = value;
              return true;
          }
          return false;
      }
  
      @Override
      public boolean setValue(int id, String value) {
          if (TransitionType.TYPE_INTERPOLATOR == id
                  || MotionType.TYPE_QUANTIZE_INTERPOLATOR_TYPE == id) {
              mQuantizeMotionInterpolator = getInterpolator(SPLINE_STRING, value, 0);
              return true;
          }
          if (MotionType.TYPE_ANIMATE_RELATIVE_TO == id) {
              mStartMotionPath.mAnimateRelativeTo = value;
              return true;
          }
          return false;
      }
  
      @Override
      public boolean setValue(int id, boolean value) {
          return false;
      }
  
      @Override
      public int getId(String name) {
          return 0;
      }
  
      /**
       * Set stagger scale
       */
      public void setStaggerScale(float staggerScale) {
          mStaggerScale = staggerScale;
      }
  
      /**
       * set the offset used in calculating stagger launches
       *
       * @param staggerOffset fraction of progress before this controller runs
       */
      public void setStaggerOffset(float staggerOffset) {
          mStaggerOffset = staggerOffset;
      }
  
      /**
       * The values set in
       * motion: {
       * stagger: '2'
       * }
       *
       * @return value from motion: { stagger: ? } or NaN if not set
       */
      public float getMotionStagger() {
          return mMotionStagger;
      }
  
      public void setIdString(String stringId) {
          mId = stringId;
          mStartMotionPath.mId = mId;
      }
  }
  