Motion.java

/*
 * 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.TypedValues;
import androidx.constraintlayout.core.motion.utils.Utils;
import androidx.constraintlayout.core.motion.utils.VelocityMatrix;
import androidx.constraintlayout.core.motion.utils.ViewState;
import androidx.constraintlayout.core.state.WidgetFrame;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;

/**
 * This contains the picture of a view through the a transition and is used to interpolate it
 * During an 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.
 *
 * @hide
 */
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 final static int DRAW_PATH_NONE = 0;
    public final static int DRAW_PATH_BASIC = 1;
    public final static int DRAW_PATH_RELATIVE = 2;
    public final static int DRAW_PATH_CARTESIAN = 3;
    public final static int DRAW_PATH_AS_CONFIGURED = 4;
    public final static int DRAW_PATH_RECTANGLE = 5;
    public final static 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;
    int mId;
    String mConstraintTag;
    private int mCurveFitType = UNSET;
    private MotionPaths mStartMotionPath = new MotionPaths();
    private MotionPaths mEndMotionPath = new MotionPaths();

    private MotionConstrainedPoint mStartPoint = new MotionConstrainedPoint();
    private MotionConstrainedPoint mEndPoint = new MotionConstrainedPoint();

    private CurveFit[] mSpline; // spline 0 is the generic one that process all the standard attributes
    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 MAX_DIMENSION = 4;
    private float mValuesBuff[] = new float[MAX_DIMENSION];
    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
    private HashMap<String, TimeCycleSplineSet> mTimeCycleAttributesMap; // splines to calculate for use TimeCycles
    private HashMap<String, SplineSet> mAttributesMap; // splines to calculate values of attributes
    private HashMap<String, KeyCycleOscillator> mCycleMap; // splines to calculate values of attributes
    private MotionKeyTrigger[] mKeyTriggers; // splines to calculate values of attributes
    private int mPathMotionArc = UNSET;
    private int mTransformPivotTarget = UNSET; // if set, pivot point is maintained as the other object
    private MotionWidget mTransformPivotView = null; // if set, pivot point is maintained as the other object
    private int mQuantizeMotionSteps = UNSET;
    private float mQuantizeMotionPhase = Float.NaN;
    private DifferentialInterpolator mQuantizeMotionInterpolator = null;
    private boolean mNoMovement = false;

    /**
     * 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 acces to MotionPath objects
     *
     * @param i
     * @return
     */
    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.x;
    }

    /**
     * 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.y;
    }

    /**
     * get the left most position of the widget at the end of the movement.
     *
     * @return the left most position
     */
    public float getFinalX() {
        return mEndMotionPath.x;
    }

    /**
     * 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.y;
    }

    /**
     * get the width of the widget at the start of the movement.
     *
     * @return the width at the start
     */
    public float getStartWidth() {
        return mStartMotionPath.width;
    }

    /**
     * get the width of the widget at the start of the movement.
     *
     * @return the height at the start
     */
    public float getStartHeight() {
        return mStartMotionPath.height;
    }

    /**
     * get the width of the widget at the end of the movement.
     *
     * @return the width at the end
     */
    public float getFinalWidth() {
        return mEndMotionPath.width;
    }

    /**
     * get the width of the widget at the end of the movement.
     *
     * @return the height at the end
     */
    public float getFinalHeight() {
        return mEndMotionPath.height;
    }

    /**
     * Will return 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 int getAnimateRelativeTo() {
        return mStartMotionPath.mAnimateRelativeTo;
    }

    public void setupRelative(Motion motionController) {
        mStartMotionPath.setupRelative(motionController, motionController.mStartMotionPath);
        mEndMotionPath.setupRelative(motionController, motionController.mEndMotionPath);
    }

    public float getCenterX() {
        return mCurrentCenterX;
    }

    public float getCenterY() {
        return mCurrentCenterY;
    }

    public void getCenter(double p, float[] pos, float[] vel) {
        double[] position = new double[4];
        double[] velocity = new double[4];
        int[] temp = new int[4];
        mSpline[0].getPos(p, position);
        mSpline[0].getSlope(p, velocity);
        Arrays.fill(vel, 0);
        mStartMotionPath.getCenter(p, mInterpolateVariables, position, pos, velocity, vel);
    }

    /**
     * fill 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
     * @param pointCount
     * @return number of key frames
     */
    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.time < position) {  // frame with easing is before the current pos
                        easing = frame.mKeyFrameEasing; // this is the candidate
                        start = frame.time; // 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.time;
                        }
                    }
                }
            }

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

    /**
     * fill 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
     * @param pointCount
     * @return number of key frames
     */
    void buildBounds(float[] bounds, 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.time < position) {  // frame with easing is before the current pos
                        easing = frame.mKeyFrameEasing; // this is the candidate
                        start = frame.time; // 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.time;
                        }
                    }
                }
            }

            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.time < position) {  // frame with easing is before the current pos
                        easing = frame.mKeyFrameEasing; // this is the candidate
                        start = frame.time; // 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.time;
                        }
                    }
                }
            }

            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 += 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.x;
        start.top = mStartMotionPath.y;
        start.right = start.left + mStartMotionPath.width;
        start.bottom = start.top + mStartMotionPath.height;
        FloatRect end = new FloatRect();
        end.left = mEndMotionPath.x;
        end.top = mEndMotionPath.y;
        end.right = end.left + mEndMotionPath.width;
        end.bottom = end.top + mEndMotionPath.height;
        for (MotionKey key : mKeyList) {
            if (key instanceof MotionKeyPosition) {
                if (((MotionKeyPosition) key).intersects(layoutWidth, layoutHeight, start, end, x, y)) {
                    return (MotionKeyPosition) key;
                }
            }
        }
        return null;
    }

    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.position);
                }
                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[] attributeTable;

    int getAttributeValues(String attributeType, float[] points, int pointCount) {
        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;
    }

    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.x - mStartMotionPath.x;
        float dy = mEndMotionPath.y - mStartMotionPath.y;
        float startCenterX = mStartMotionPath.x + mStartMotionPath.width / 2;
        float startCenterY = mStartMotionPath.y + mStartMotionPath.height / 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.position == p.position) {
                redundant = p;
            }
        }
        if (redundant != null) {
            mMotionPaths.remove(redundant);
        }
        int pos = Collections.binarySearch(mMotionPaths, point);
        if (pos == 0) {
            Utils.loge(TAG, " KeyPath position \"" + point.position + "\" 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());
            }
        }
    }

    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) {
        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;
        if (DEBUG) {
            if (mKeyList == null) {
                Utils.log(TAG, ">>>>>>>>>>>>>>> mKeyList==null");

            } else {
                Utils.log(TAG, ">>>>>>>>>>>>>>> mKeyList for " + mView.getName());

            }
        }

        if (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.customAttributes.keySet()) {
            if (mStartMotionPath.customAttributes.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].customAttributes.containsKey(attributeName)) {
                    CustomVariable attribute = points[j].customAttributes.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].time;
        }

        for (int j = 0; j < mInterpolateVariables.length; j++) {
            int interpolateVariable = mInterpolateVariables[j];
            if (interpolateVariable < MotionPaths.names.length) {
                String s = MotionPaths.names[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].time;
                    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);
        }

        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].time;
                values[i][0] = points[i].x;
                values[i][1] = points[i].y;
            }

            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
     *
     * @return
     */
    public String toString() {
        return " start: x: " + mStartMotionPath.x + " y: " + mStartMotionPath.y
                + " end: x: " + mEndMotionPath.x + " y: " + mEndMotionPath.y;
    }

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

    public void setStart(MotionWidget mw) {
        mStartMotionPath.time = 0;
        mStartMotionPath.position = 0;
        mStartMotionPath.setBounds(mw.getX(), mw.getY(), mw.getWidth(), mw.getHeight());
        mStartMotionPath.applyParameters(mw);
        mStartPoint.setState(mw);
    }

    public void setEnd(MotionWidget mw) {
        mEndMotionPath.time = 1;
        mEndMotionPath.position = 1;
        readView(mEndMotionPath);
        mEndMotionPath.setBounds(mw.getLeft(), mw.getTop(), mw.getWidth(), mw.getHeight());
        mEndMotionPath.applyParameters(mw);
        mEndPoint.setState(mw);
    }

    public void setStartState(ViewState rect, MotionWidget v, int rotation, int preWidth, int preHeight) {
        mStartMotionPath.time = 0;
        mStartMotionPath.position = 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;
    private static final int INTERPOLATOR_REFERENCE_ID = -2;
    private static final int INTERPOLATOR_UNDEFINED = -3;

    private static DifferentialInterpolator getInterpolator(int type, String interpolatorString, 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.time = 0;
        mStartMotionPath.position = 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 (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.time < position) {  // frame with easing is before the current pos
                    easing = frame.mKeyFrameEasing; // this is the candidate
                    start = frame.time; // 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.time;
                    }
                }
            }
        }

        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
     *
     * @param child
     * @param global_position
     * @param time
     * @param keyCache
     * @return do you need to keep animating
     */
    public boolean interpolate(MotionWidget child, float global_position, long time, KeyCache keyCache) {
        boolean timeAnimation = false;
        float position = getAdjustedPosition(global_position, null);
        // This quantize the position into steps e.g 4 steps = 0-0.25,0.25-0.50 etc
        if (mQuantizeMotionSteps != UNSET) {
            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.customAttributes.get(mAttributeNames[i - 1]).setInterpolatedValue(child, mValuesBuff);
            }
            if (mStartPoint.mVisibilityMode == MotionWidget.VISIBILITY_MODE_NORMAL) {
                if (position <= 0.0f) {
                    child.setVisibility(mStartPoint.visibility);
                } else if (position >= 1.0f) {
                    child.setVisibility(mEndPoint.visibility);
                } else if (mEndPoint.visibility != mStartPoint.visibility) {
                    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.x + (mEndMotionPath.x - mStartMotionPath.x) * position);
            float float_t = (mStartMotionPath.y + (mEndMotionPath.y - mStartMotionPath.y) * position);
            float float_width = (mStartMotionPath.width + (mEndMotionPath.width - mStartMotionPath.width) * position);
            float float_height = (mStartMotionPath.height + (mEndMotionPath.height - mStartMotionPath.height) * 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.x + (mEndMotionPath.x - mStartMotionPath.x) * position);
                t = (int) (mStartMotionPath.y + (mEndMotionPath.y - mStartMotionPath.y) * position);
                width = (int) (mStartMotionPath.width + (mEndMotionPath.width - mStartMotionPath.width) * position);
                height = (int) (mStartMotionPath.height + (mEndMotionPath.height - mStartMotionPath.height) * 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
     */
    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.x - mStartMotionPath.x);
        float dTop = (mEndMotionPath.y - mStartMotionPath.y);
        float dWidth = (mEndMotionPath.width - mStartMotionPath.width);
        float dHeight = (mEndMotionPath.height - mStartMotionPath.height);
        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.x - mStartMotionPath.x);
        float dTop = (mEndMotionPath.y - mStartMotionPath.y);
        float dWidth = (mEndMotionPath.width - mStartMotionPath.width);
        float dHeight = (mEndMotionPath.height - mStartMotionPath.height);
        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;
    }

    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.x;
        start.top = mStartMotionPath.y;
        start.right = start.left + mStartMotionPath.width;
        start.bottom = start.top + mStartMotionPath.height;
        FloatRect end = new FloatRect();
        end.left = mEndMotionPath.x;
        end.top = mEndMotionPath.y;
        end.right = end.left + mEndMotionPath.width;
        end.bottom = end.top + mEndMotionPath.height;
        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;
    }

    /**
     * Get the keyFrames for the view controlled by this MotionController
     * the info data structure is of the 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.TransitionType.TYPE_AUTO_TRANSITION:
               // TODO add support for auto transitions mAutoTransition = value;
                return true;
        }
        return false;
    }

    @Override
    public boolean setValue(int id, float value) {
        return false;
    }

    @Override
    public boolean setValue(int id, String value) {
        if (TransitionType.TYPE_INTERPOLATOR == id) {
            System.out.println("TYPE_INTERPOLATOR  "+value);
           mQuantizeMotionInterpolator = getInterpolator(SPLINE_STRING, value,0);
        }
        return false;
    }

    @Override
    public boolean setValue(int id, boolean value) {
        return false;
    }

    @Override
    public int getId(String name) {
        return 0;
    }
}