Use optimized Keyframes for Path animations.

Bug 17005728

Change-Id: I2e109ed1a3e768e1e0286fc3950516f16509e591
diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java
index e57be83..f4e4671 100644
--- a/core/java/android/animation/AnimatorInflater.java
+++ b/core/java/android/animation/AnimatorInflater.java
@@ -370,14 +370,23 @@
                         + " propertyXName or propertyYName is needed for PathData");
             } else {
                 Path path = PathParser.createPathFromPathData(pathData);
-                Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, !getFloats);
+                PathKeyframes keyframeSet = KeyframeSet.ofPath(path);
+                Keyframes xKeyframes;
+                Keyframes yKeyframes;
+                if (getFloats) {
+                    xKeyframes = keyframeSet.createXFloatKeyframes();
+                    yKeyframes = keyframeSet.createYFloatKeyframes();
+                } else {
+                    xKeyframes = keyframeSet.createXIntKeyframes();
+                    yKeyframes = keyframeSet.createYIntKeyframes();
+                }
                 PropertyValuesHolder x = null;
                 PropertyValuesHolder y = null;
                 if (propertyXName != null) {
-                    x = PropertyValuesHolder.ofKeyframe(propertyXName, keyframes[0]);
+                    x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes);
                 }
                 if (propertyYName != null) {
-                    y = PropertyValuesHolder.ofKeyframe(propertyYName, keyframes[1]);
+                    y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes);
                 }
                 if (x == null) {
                     oa.setValues(y);
diff --git a/core/java/android/animation/FloatKeyframeSet.java b/core/java/android/animation/FloatKeyframeSet.java
index 2d87e13..12e5862 100644
--- a/core/java/android/animation/FloatKeyframeSet.java
+++ b/core/java/android/animation/FloatKeyframeSet.java
@@ -30,7 +30,7 @@
  * TypeEvaluator set for the animation, so that values can be calculated without autoboxing to the
  * Object equivalents of these primitive types.</p>
  */
-class FloatKeyframeSet extends KeyframeSet {
+class FloatKeyframeSet extends KeyframeSet implements Keyframes.FloatKeyframes {
     private float firstValue;
     private float lastValue;
     private float deltaValue;
@@ -58,10 +58,11 @@
     }
 
     @Override
-    void invalidateCache() {
+    public void invalidateCache() {
         firstTime = true;
     }
 
+    @Override
     public float getFloatValue(float fraction) {
         if (mNumKeyframes == 2) {
             if (firstTime) {
@@ -135,5 +136,9 @@
         return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue();
     }
 
+    @Override
+    public Class getType() {
+        return Float.class;
+    }
 }
 
diff --git a/core/java/android/animation/IntKeyframeSet.java b/core/java/android/animation/IntKeyframeSet.java
index ce47e2b..7a5b0ec 100644
--- a/core/java/android/animation/IntKeyframeSet.java
+++ b/core/java/android/animation/IntKeyframeSet.java
@@ -30,7 +30,7 @@
  * TypeEvaluator set for the animation, so that values can be calculated without autoboxing to the
  * Object equivalents of these primitive types.</p>
  */
-class IntKeyframeSet extends KeyframeSet {
+class IntKeyframeSet extends KeyframeSet implements Keyframes.IntKeyframes {
     private int firstValue;
     private int lastValue;
     private int deltaValue;
@@ -58,10 +58,11 @@
     }
 
     @Override
-    void invalidateCache() {
+    public void invalidateCache() {
         firstTime = true;
     }
 
+    @Override
     public int getIntValue(float fraction) {
         if (mNumKeyframes == 2) {
             if (firstTime) {
@@ -134,5 +135,9 @@
         return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue();
     }
 
+    @Override
+    public Class getType() {
+        return Integer.class;
+    }
 }
 
diff --git a/core/java/android/animation/KeyframeSet.java b/core/java/android/animation/KeyframeSet.java
index a3db3a1..fc9bbb1 100644
--- a/core/java/android/animation/KeyframeSet.java
+++ b/core/java/android/animation/KeyframeSet.java
@@ -21,6 +21,7 @@
 import android.animation.Keyframe.IntKeyframe;
 import android.animation.Keyframe.FloatKeyframe;
 import android.animation.Keyframe.ObjectKeyframe;
+import android.graphics.Path;
 import android.util.Log;
 
 /**
@@ -28,7 +29,7 @@
  * values between those keyframes for a given animation. The class internal to the animation
  * package because it is an implementation detail of how Keyframes are stored and used.
  */
-class KeyframeSet {
+class KeyframeSet implements Keyframes {
 
     int mNumKeyframes;
 
@@ -52,7 +53,12 @@
      * If subclass has variables that it calculates based on the Keyframes, it should reset them
      * when this method is called because Keyframe contents might have changed.
      */
-    void invalidateCache() {
+    @Override
+    public void invalidateCache() {
+    }
+
+    public ArrayList<Keyframe> getKeyframes() {
+        return mKeyframes;
     }
 
     public static KeyframeSet ofInt(int... values) {
@@ -144,6 +150,10 @@
         return new KeyframeSet(keyframes);
     }
 
+    public static PathKeyframes ofPath(Path path) {
+        return new PathKeyframes(path);
+    }
+
     /**
      * Sets the TypeEvaluator to be used when calculating animated values. This object
      * is required only for KeyframeSets that are not either IntKeyframeSet or FloatKeyframeSet,
@@ -157,6 +167,11 @@
     }
 
     @Override
+    public Class getType() {
+        return mFirstKeyframe.getType();
+    }
+
+    @Override
     public KeyframeSet clone() {
         ArrayList<Keyframe> keyframes = mKeyframes;
         int numKeyframes = mKeyframes.size();
diff --git a/core/java/android/animation/Keyframes.java b/core/java/android/animation/Keyframes.java
new file mode 100644
index 0000000..6611c6c
--- /dev/null
+++ b/core/java/android/animation/Keyframes.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2014 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 android.animation;
+
+import java.util.ArrayList;
+
+/**
+ * This interface abstracts a collection of Keyframe objects and is called by
+ * ValueAnimator to calculate values between those keyframes for a given animation.
+ */
+interface Keyframes extends Cloneable {
+
+    /**
+     * Sets the TypeEvaluator to be used when calculating animated values. This object
+     * is required only for Keyframes that are not either IntKeyframes or FloatKeyframes,
+     * both of which assume their own evaluator to speed up calculations with those primitive
+     * types.
+     *
+     * @param evaluator The TypeEvaluator to be used to calculate animated values.
+     */
+    void setEvaluator(TypeEvaluator evaluator);
+
+    /**
+     * @return The value type contained by the contained Keyframes.
+     */
+    Class getType();
+
+    /**
+     * Gets the animated value, given the elapsed fraction of the animation (interpolated by the
+     * animation's interpolator) and the evaluator used to calculate in-between values. This
+     * function maps the input fraction to the appropriate keyframe interval and a fraction
+     * between them and returns the interpolated value. Note that the input fraction may fall
+     * outside the [0-1] bounds, if the animation's interpolator made that happen (e.g., a
+     * spring interpolation that might send the fraction past 1.0). We handle this situation by
+     * just using the two keyframes at the appropriate end when the value is outside those bounds.
+     *
+     * @param fraction The elapsed fraction of the animation
+     * @return The animated value.
+     */
+    Object getValue(float fraction);
+
+    /**
+     * If subclass has variables that it calculates based on the Keyframes, it should reset them
+     * when this method is called because Keyframe contents might have changed.
+     */
+    void invalidateCache();
+
+    /**
+     * @return A list of all Keyframes contained by this. This may return null if this is
+     * not made up of Keyframes.
+     */
+    ArrayList<Keyframe> getKeyframes();
+
+    Keyframes clone();
+
+    /**
+     * A specialization of Keyframes that has integer primitive value calculation.
+     */
+    public interface IntKeyframes extends Keyframes {
+
+        /**
+         * Works like {@link #getValue(float)}, but returning a primitive.
+         * @param fraction The elapsed fraction of the animation
+         * @return The animated value.
+         */
+        int getIntValue(float fraction);
+    }
+
+    /**
+     * A specialization of Keyframes that has float primitive value calculation.
+     */
+    public interface FloatKeyframes extends Keyframes {
+
+        /**
+         * Works like {@link #getValue(float)}, but returning a primitive.
+         * @param fraction The elapsed fraction of the animation
+         * @return The animated value.
+         */
+        float getFloatValue(float fraction);
+    }
+}
diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java
index 188408d..5790682 100644
--- a/core/java/android/animation/LayoutTransition.java
+++ b/core/java/android/animation/LayoutTransition.java
@@ -899,11 +899,15 @@
                     PropertyValuesHolder[] oldValues = valueAnim.getValues();
                     for (int i = 0; i < oldValues.length; ++i) {
                         PropertyValuesHolder pvh = oldValues[i];
-                        KeyframeSet keyframeSet = pvh.mKeyframeSet;
-                        if (keyframeSet.mFirstKeyframe == null ||
-                                keyframeSet.mLastKeyframe == null ||
-                                !keyframeSet.mFirstKeyframe.getValue().equals(
-                                keyframeSet.mLastKeyframe.getValue())) {
+                        if (pvh.mKeyframes instanceof KeyframeSet) {
+                            KeyframeSet keyframeSet = (KeyframeSet) pvh.mKeyframes;
+                            if (keyframeSet.mFirstKeyframe == null ||
+                                    keyframeSet.mLastKeyframe == null ||
+                                    !keyframeSet.mFirstKeyframe.getValue().equals(
+                                            keyframeSet.mLastKeyframe.getValue())) {
+                                valuesDiffer = true;
+                            }
+                        } else if (!pvh.mKeyframes.getValue(0).equals(pvh.mKeyframes.getValue(1))) {
                             valuesDiffer = true;
                         }
                     }
diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java
index a4ac73f..500634c 100644
--- a/core/java/android/animation/ObjectAnimator.java
+++ b/core/java/android/animation/ObjectAnimator.java
@@ -239,9 +239,11 @@
      */
     public static ObjectAnimator ofInt(Object target, String xPropertyName, String yPropertyName,
             Path path) {
-        Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, true);
-        PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xPropertyName, keyframes[0]);
-        PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yPropertyName, keyframes[1]);
+        PathKeyframes keyframes = KeyframeSet.ofPath(path);
+        PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xPropertyName,
+                keyframes.createXIntKeyframes());
+        PropertyValuesHolder y = PropertyValuesHolder.ofKeyframes(yPropertyName,
+                keyframes.createYIntKeyframes());
         return ofPropertyValuesHolder(target, x, y);
     }
 
@@ -278,9 +280,11 @@
      */
     public static <T> ObjectAnimator ofInt(T target, Property<T, Integer> xProperty,
             Property<T, Integer> yProperty, Path path) {
-        Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, true);
-        PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xProperty, keyframes[0]);
-        PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yProperty, keyframes[1]);
+        PathKeyframes keyframes = KeyframeSet.ofPath(path);
+        PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xProperty,
+                keyframes.createXIntKeyframes());
+        PropertyValuesHolder y = PropertyValuesHolder.ofKeyframes(yProperty,
+                keyframes.createYIntKeyframes());
         return ofPropertyValuesHolder(target, x, y);
     }
 
@@ -429,9 +433,11 @@
      */
     public static ObjectAnimator ofFloat(Object target, String xPropertyName, String yPropertyName,
             Path path) {
-        Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, false);
-        PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xPropertyName, keyframes[0]);
-        PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yPropertyName, keyframes[1]);
+        PathKeyframes keyframes = KeyframeSet.ofPath(path);
+        PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xPropertyName,
+                keyframes.createXFloatKeyframes());
+        PropertyValuesHolder y = PropertyValuesHolder.ofKeyframes(yPropertyName,
+                keyframes.createYFloatKeyframes());
         return ofPropertyValuesHolder(target, x, y);
     }
 
@@ -469,9 +475,11 @@
      */
     public static <T> ObjectAnimator ofFloat(T target, Property<T, Float> xProperty,
             Property<T, Float> yProperty, Path path) {
-        Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, false);
-        PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xProperty, keyframes[0]);
-        PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yProperty, keyframes[1]);
+        PathKeyframes keyframes = KeyframeSet.ofPath(path);
+        PropertyValuesHolder x = PropertyValuesHolder.ofKeyframes(xProperty,
+                keyframes.createXFloatKeyframes());
+        PropertyValuesHolder y = PropertyValuesHolder.ofKeyframes(yProperty,
+                keyframes.createYFloatKeyframes());
         return ofPropertyValuesHolder(target, x, y);
     }
 
@@ -652,6 +660,10 @@
      * uses a type other than <code>PointF</code>, <code>converter</code> can be used to change
      * from <code>PointF</code> to the type associated with the <code>Property</code>.
      *
+     * <p>The PointF passed to <code>converter</code> or <code>property</code>, if
+     * <code>converter</code> is <code>null</code>, is reused on each animation frame and should
+     * not be stored by the setter or TypeConverter.</p>
+     *
      * @param target The object whose property is to be animated.
      * @param property The property being animated. Should not be null.
      * @param converter Converts a PointF to the type associated with the setter. May be
@@ -809,10 +821,9 @@
             Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
             for (int i = 0; i < mValues.length; ++i) {
                 PropertyValuesHolder pvh = mValues[i];
-                ArrayList<Keyframe> keyframes = pvh.mKeyframeSet.mKeyframes;
                 Log.d(LOG_TAG, "   Values[" + i + "]: " +
-                    pvh.getPropertyName() + ", " + keyframes.get(0).getValue() + ", " +
-                    keyframes.get(pvh.mKeyframeSet.mNumKeyframes - 1).getValue());
+                    pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
+                    pvh.mKeyframes.getValue(1));
             }
         }
         super.start();
diff --git a/core/java/android/animation/PathKeyframes.java b/core/java/android/animation/PathKeyframes.java
new file mode 100644
index 0000000..70eed90
--- /dev/null
+++ b/core/java/android/animation/PathKeyframes.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2014 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 android.animation;
+
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.util.MathUtils;
+
+import java.util.ArrayList;
+
+/**
+ * PathKeyframes relies on approximating the Path as a series of line segments.
+ * The line segments are recursively divided until there is less than 1/2 pixel error
+ * between the lines and the curve. Each point of the line segment is converted
+ * to a Keyframe and a linear interpolation between Keyframes creates a good approximation
+ * of the curve.
+ * <p>
+ * PathKeyframes is optimized to reduce the number of objects created when there are
+ * many keyframes for a curve.
+ * </p>
+ * <p>
+ * Typically, the returned type is a PointF, but the individual components can be extracted
+ * as either an IntKeyframes or FloatKeyframes.
+ * </p>
+ */
+class PathKeyframes implements Keyframes {
+    private static final int FRACTION_OFFSET = 0;
+    private static final int X_OFFSET = 1;
+    private static final int Y_OFFSET = 2;
+    private static final int NUM_COMPONENTS = 3;
+    private static final ArrayList<Keyframe> EMPTY_KEYFRAMES = new ArrayList<Keyframe>();
+
+    private PointF mTempPointF = new PointF();
+    private float[] mKeyframeData;
+
+    public PathKeyframes(Path path) {
+        this(path, 0.5f);
+    }
+
+    public PathKeyframes(Path path, float error) {
+        if (path == null || path.isEmpty()) {
+            throw new IllegalArgumentException("The path must not be null or empty");
+        }
+        mKeyframeData = path.approximate(error);
+    }
+
+    @Override
+    public ArrayList<Keyframe> getKeyframes() {
+        return EMPTY_KEYFRAMES;
+    }
+
+    @Override
+    public Object getValue(float fraction) {
+        fraction = MathUtils.constrain(fraction, 0, 1);
+
+        int numPoints = mKeyframeData.length / 3;
+
+        if (fraction == 0) {
+            return pointForIndex(0);
+        } else if (fraction == 1) {
+            return pointForIndex(numPoints - 1);
+        } else {
+            // Binary search for the correct section
+            int low = 0;
+            int high = numPoints - 1;
+
+            while (low <= high) {
+                int mid = (low + high) / 2;
+                float midFraction = mKeyframeData[(mid * NUM_COMPONENTS) + FRACTION_OFFSET];
+
+                if (fraction < midFraction) {
+                    high = mid - 1;
+                } else if (fraction > midFraction) {
+                    low = mid + 1;
+                } else {
+                    return pointForIndex(mid);
+                }
+            }
+
+            // now high is below the fraction and low is above the fraction
+            int startBase = (high * NUM_COMPONENTS);
+            int endBase = (low * NUM_COMPONENTS);
+
+            float startFraction = mKeyframeData[startBase + FRACTION_OFFSET];
+            float endFraction = mKeyframeData[endBase + FRACTION_OFFSET];
+
+            float intervalFraction = (fraction - startFraction)/(endFraction - startFraction);
+
+            float startX = mKeyframeData[startBase + X_OFFSET];
+            float endX = mKeyframeData[endBase + X_OFFSET];
+            float startY = mKeyframeData[startBase + Y_OFFSET];
+            float endY = mKeyframeData[endBase + Y_OFFSET];
+
+            float x = interpolate(intervalFraction, startX, endX);
+            float y = interpolate(intervalFraction, startY, endY);
+
+            mTempPointF.set(x, y);
+            return mTempPointF;
+        }
+    }
+
+    @Override
+    public void invalidateCache() {
+    }
+
+    @Override
+    public void setEvaluator(TypeEvaluator evaluator) {
+    }
+
+    @Override
+    public Class getType() {
+        return PointF.class;
+    }
+
+    @Override
+    public Keyframes clone() {
+        Keyframes clone = null;
+        try {
+            clone = (Keyframes) super.clone();
+        } catch (CloneNotSupportedException e) {}
+        return clone;
+    }
+
+    private PointF pointForIndex(int index) {
+        int base = (index * NUM_COMPONENTS);
+        int xOffset = base + X_OFFSET;
+        int yOffset = base + Y_OFFSET;
+        mTempPointF.set(mKeyframeData[xOffset], mKeyframeData[yOffset]);
+        return mTempPointF;
+    }
+
+    private static float interpolate(float fraction, float startValue, float endValue) {
+        float diff = endValue - startValue;
+        return startValue + (diff * fraction);
+    }
+
+    /**
+     * Returns a FloatKeyframes for the X component of the Path.
+     * @return a FloatKeyframes for the X component of the Path.
+     */
+    public FloatKeyframes createXFloatKeyframes() {
+        return new FloatKeyframesBase() {
+            @Override
+            public float getFloatValue(float fraction) {
+                PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
+                return pointF.x;
+            }
+        };
+    }
+
+    /**
+     * Returns a FloatKeyframes for the Y component of the Path.
+     * @return a FloatKeyframes for the Y component of the Path.
+     */
+    public FloatKeyframes createYFloatKeyframes() {
+        return new FloatKeyframesBase() {
+            @Override
+            public float getFloatValue(float fraction) {
+                PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
+                return pointF.y;
+            }
+        };
+    }
+
+    /**
+     * Returns an IntKeyframes for the X component of the Path.
+     * @return an IntKeyframes for the X component of the Path.
+     */
+    public IntKeyframes createXIntKeyframes() {
+        return new IntKeyframesBase() {
+            @Override
+            public int getIntValue(float fraction) {
+                PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
+                return Math.round(pointF.x);
+            }
+        };
+    }
+
+    /**
+     * Returns an IntKeyframeSet for the Y component of the Path.
+     * @return an IntKeyframeSet for the Y component of the Path.
+     */
+    public IntKeyframes createYIntKeyframes() {
+        return new IntKeyframesBase() {
+            @Override
+            public int getIntValue(float fraction) {
+                PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
+                return Math.round(pointF.y);
+            }
+        };
+    }
+
+    private abstract static class SimpleKeyframes implements Keyframes {
+        @Override
+        public void setEvaluator(TypeEvaluator evaluator) {
+        }
+
+        @Override
+        public void invalidateCache() {
+        }
+
+        @Override
+        public ArrayList<Keyframe> getKeyframes() {
+            return EMPTY_KEYFRAMES;
+        }
+
+        @Override
+        public Keyframes clone() {
+            Keyframes clone = null;
+            try {
+                clone = (Keyframes) super.clone();
+            } catch (CloneNotSupportedException e) {}
+            return clone;
+        }
+    }
+
+    private abstract static class IntKeyframesBase extends SimpleKeyframes implements IntKeyframes {
+        @Override
+        public Class getType() {
+            return Integer.class;
+        }
+
+        @Override
+        public Object getValue(float fraction) {
+            return getIntValue(fraction);
+        }
+    }
+
+    private abstract static class FloatKeyframesBase extends SimpleKeyframes
+            implements FloatKeyframes {
+        @Override
+        public Class getType() {
+            return Float.class;
+        }
+
+        @Override
+        public Object getValue(float fraction) {
+            return getFloatValue(fraction);
+        }
+    }
+}
diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java
index 73b83ef..d372933 100644
--- a/core/java/android/animation/PropertyValuesHolder.java
+++ b/core/java/android/animation/PropertyValuesHolder.java
@@ -18,7 +18,6 @@
 
 import android.graphics.Path;
 import android.graphics.PointF;
-import android.util.FloatMath;
 import android.util.FloatProperty;
 import android.util.IntProperty;
 import android.util.Log;
@@ -26,6 +25,7 @@
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
@@ -75,7 +75,7 @@
     /**
      * The set of keyframes (time/value pairs) that define this animation.
      */
-    KeyframeSet mKeyframeSet = null;
+    Keyframes mKeyframes = null;
 
 
     // type evaluators for the primitive types handled by this implementation
@@ -219,11 +219,9 @@
      * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
      */
     public static PropertyValuesHolder ofMultiInt(String propertyName, Path path) {
-        Keyframe[] keyframes = createKeyframes(path);
-        KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(keyframes);
-        TypeEvaluator<PointF> evaluator = new PointFEvaluator(new PointF());
+        Keyframes keyframes = KeyframeSet.ofPath(path);
         PointFToIntArray converter = new PointFToIntArray();
-        return new MultiIntValuesHolder(propertyName, converter, evaluator, keyframeSet);
+        return new MultiIntValuesHolder(propertyName, converter, null, keyframes);
     }
 
     /**
@@ -339,11 +337,9 @@
      * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
      */
     public static PropertyValuesHolder ofMultiFloat(String propertyName, Path path) {
-        Keyframe[] keyframes = createKeyframes(path);
-        KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(keyframes);
-        TypeEvaluator<PointF> evaluator = new PointFEvaluator(new PointF());
+        Keyframes keyframes = KeyframeSet.ofPath(path);
         PointFToFloatArray converter = new PointFToFloatArray();
-        return new MultiFloatValuesHolder(propertyName, converter, evaluator, keyframeSet);
+        return new MultiFloatValuesHolder(propertyName, converter, null, keyframes);
     }
 
     /**
@@ -415,6 +411,10 @@
      * <code>TypeConverter</code> to convert from <code>PointF</code> to the target
      * type.
      *
+     * <p>The PointF passed to <code>converter</code> or <code>property</code>, if
+     * <code>converter</code> is <code>null</code>, is reused on each animation frame and should
+     * not be stored by the setter or TypeConverter.</p>
+     *
      * @param propertyName The name of the property being animated.
      * @param converter Converts a PointF to the type associated with the setter. May be
      *                  null if conversion is unnecessary.
@@ -423,9 +423,9 @@
      */
     public static PropertyValuesHolder ofObject(String propertyName,
             TypeConverter<PointF, ?> converter, Path path) {
-        Keyframe[] keyframes = createKeyframes(path);
-        PropertyValuesHolder pvh = ofKeyframe(propertyName, keyframes);
-        pvh.setEvaluator(new PointFEvaluator(new PointF()));
+        PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
+        pvh.mKeyframes = KeyframeSet.ofPath(path);
+        pvh.mValueType = PointF.class;
         pvh.setConverter(converter);
         return pvh;
     }
@@ -484,6 +484,10 @@
      * <code>TypeConverter</code> to convert from <code>PointF</code> to the target
      * type.
      *
+     * <p>The PointF passed to <code>converter</code> or <code>property</code>, if
+     * <code>converter</code> is <code>null</code>, is reused on each animation frame and should
+     * not be stored by the setter or TypeConverter.</p>
+     *
      * @param property The property being animated. Should not be null.
      * @param converter Converts a PointF to the type associated with the setter. May be
      *                  null if conversion is unnecessary.
@@ -492,9 +496,9 @@
      */
     public static <V> PropertyValuesHolder ofObject(Property<?, V> property,
             TypeConverter<PointF, V> converter, Path path) {
-        Keyframe[] keyframes = createKeyframes(path);
-        PropertyValuesHolder pvh = ofKeyframe(property, keyframes);
-        pvh.setEvaluator(new PointFEvaluator(new PointF()));
+        PropertyValuesHolder pvh = new PropertyValuesHolder(property);
+        pvh.mKeyframes = KeyframeSet.ofPath(path);
+        pvh.mValueType = PointF.class;
         pvh.setConverter(converter);
         return pvh;
     }
@@ -520,17 +524,7 @@
      */
     public static PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values) {
         KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
-        if (keyframeSet instanceof IntKeyframeSet) {
-            return new IntPropertyValuesHolder(propertyName, (IntKeyframeSet) keyframeSet);
-        } else if (keyframeSet instanceof FloatKeyframeSet) {
-            return new FloatPropertyValuesHolder(propertyName, (FloatKeyframeSet) keyframeSet);
-        }
-        else {
-            PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
-            pvh.mKeyframeSet = keyframeSet;
-            pvh.mValueType = ((Keyframe)values[0]).getType();
-            return pvh;
-        }
+        return ofKeyframes(propertyName, keyframeSet);
     }
 
     /**
@@ -551,15 +545,32 @@
      */
     public static PropertyValuesHolder ofKeyframe(Property property, Keyframe... values) {
         KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
-        if (keyframeSet instanceof IntKeyframeSet) {
-            return new IntPropertyValuesHolder(property, (IntKeyframeSet) keyframeSet);
-        } else if (keyframeSet instanceof FloatKeyframeSet) {
-            return new FloatPropertyValuesHolder(property, (FloatKeyframeSet) keyframeSet);
+        return ofKeyframes(property, keyframeSet);
+    }
+
+    static PropertyValuesHolder ofKeyframes(String propertyName, Keyframes keyframes) {
+        if (keyframes instanceof Keyframes.IntKeyframes) {
+            return new IntPropertyValuesHolder(propertyName, (Keyframes.IntKeyframes) keyframes);
+        } else if (keyframes instanceof Keyframes.FloatKeyframes) {
+            return new FloatPropertyValuesHolder(propertyName,
+                    (Keyframes.FloatKeyframes) keyframes);
+        } else {
+            PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
+            pvh.mKeyframes = keyframes;
+            pvh.mValueType = keyframes.getType();
+            return pvh;
         }
-        else {
+    }
+
+    static PropertyValuesHolder ofKeyframes(Property property, Keyframes keyframes) {
+        if (keyframes instanceof Keyframes.IntKeyframes) {
+            return new IntPropertyValuesHolder(property, (Keyframes.IntKeyframes) keyframes);
+        } else if (keyframes instanceof Keyframes.FloatKeyframes) {
+            return new FloatPropertyValuesHolder(property, (Keyframes.FloatKeyframes) keyframes);
+        } else {
             PropertyValuesHolder pvh = new PropertyValuesHolder(property);
-            pvh.mKeyframeSet = keyframeSet;
-            pvh.mValueType = ((Keyframe)values[0]).getType();
+            pvh.mKeyframes = keyframes;
+            pvh.mValueType = keyframes.getType();
             return pvh;
         }
     }
@@ -579,7 +590,7 @@
      */
     public void setIntValues(int... values) {
         mValueType = int.class;
-        mKeyframeSet = KeyframeSet.ofInt(values);
+        mKeyframes = KeyframeSet.ofInt(values);
     }
 
     /**
@@ -597,7 +608,7 @@
      */
     public void setFloatValues(float... values) {
         mValueType = float.class;
-        mKeyframeSet = KeyframeSet.ofFloat(values);
+        mKeyframes = KeyframeSet.ofFloat(values);
     }
 
     /**
@@ -612,7 +623,7 @@
         for (int i = 0; i < numKeyframes; ++i) {
             keyframes[i] = (Keyframe)values[i];
         }
-        mKeyframeSet = new KeyframeSet(keyframes);
+        mKeyframes = new KeyframeSet(keyframes);
     }
 
     /**
@@ -630,9 +641,9 @@
      */
     public void setObjectValues(Object... values) {
         mValueType = values[0].getClass();
-        mKeyframeSet = KeyframeSet.ofObject(values);
+        mKeyframes = KeyframeSet.ofObject(values);
         if (mEvaluator != null) {
-            mKeyframeSet.setEvaluator(mEvaluator);
+            mKeyframes.setEvaluator(mEvaluator);
         }
     }
 
@@ -775,12 +786,15 @@
      * @param target The object on which the setter (and possibly getter) exist.
      */
     void setupSetterAndGetter(Object target) {
-        mKeyframeSet.invalidateCache();
+        mKeyframes.invalidateCache();
         if (mProperty != null) {
             // check to make sure that mProperty is on the class of target
             try {
                 Object testValue = null;
-                for (Keyframe kf : mKeyframeSet.mKeyframes) {
+                ArrayList<Keyframe> keyframes = mKeyframes.getKeyframes();
+                int keyframeCount = keyframes == null ? 0 : keyframes.size();
+                for (int i = 0; i < keyframeCount; i++) {
+                    Keyframe kf = keyframes.get(i);
                     if (!kf.hasValue() || kf.valueWasSetOnStart()) {
                         if (testValue == null) {
                             testValue = convertBack(mProperty.get(target));
@@ -800,7 +814,10 @@
         if (mSetter == null) {
             setupSetter(targetClass);
         }
-        for (Keyframe kf : mKeyframeSet.mKeyframes) {
+        ArrayList<Keyframe> keyframes = mKeyframes.getKeyframes();
+        int keyframeCount = keyframes == null ? 0 : keyframes.size();
+        for (int i = 0; i < keyframeCount; i++) {
+            Keyframe kf = keyframes.get(i);
             if (!kf.hasValue() || kf.valueWasSetOnStart()) {
                 if (mGetter == null) {
                     setupGetter(targetClass);
@@ -873,7 +890,10 @@
      * @param target The object which holds the start values that should be set.
      */
     void setupStartValue(Object target) {
-        setupValue(target, mKeyframeSet.mKeyframes.get(0));
+        ArrayList<Keyframe> keyframes = mKeyframes.getKeyframes();
+        if (!keyframes.isEmpty()) {
+            setupValue(target, keyframes.get(0));
+        }
     }
 
     /**
@@ -885,7 +905,10 @@
      * @param target The object which holds the start values that should be set.
      */
     void setupEndValue(Object target) {
-        setupValue(target, mKeyframeSet.mKeyframes.get(mKeyframeSet.mKeyframes.size() - 1));
+        ArrayList<Keyframe> keyframes = mKeyframes.getKeyframes();
+        if (!keyframes.isEmpty()) {
+            setupValue(target, keyframes.get(keyframes.size() - 1));
+        }
     }
 
     @Override
@@ -894,7 +917,7 @@
             PropertyValuesHolder newPVH = (PropertyValuesHolder) super.clone();
             newPVH.mPropertyName = mPropertyName;
             newPVH.mProperty = mProperty;
-            newPVH.mKeyframeSet = mKeyframeSet.clone();
+            newPVH.mKeyframes = mKeyframes.clone();
             newPVH.mEvaluator = mEvaluator;
             return newPVH;
         } catch (CloneNotSupportedException e) {
@@ -941,7 +964,7 @@
         if (mEvaluator != null) {
             // KeyframeSet knows how to evaluate the common types - only give it a custom
             // evaluator if one has been set on this class
-            mKeyframeSet.setEvaluator(mEvaluator);
+            mKeyframes.setEvaluator(mEvaluator);
         }
     }
 
@@ -957,7 +980,7 @@
      */
     public void setEvaluator(TypeEvaluator evaluator) {
         mEvaluator = evaluator;
-        mKeyframeSet.setEvaluator(evaluator);
+        mKeyframes.setEvaluator(evaluator);
     }
 
     /**
@@ -967,7 +990,7 @@
      * @param fraction The elapsed, interpolated fraction of the animation.
      */
     void calculateValue(float fraction) {
-        Object value = mKeyframeSet.getValue(fraction);
+        Object value = mKeyframes.getValue(fraction);
         mAnimatedValue = mConverter == null ? value : mConverter.convert(value);
     }
 
@@ -1025,7 +1048,7 @@
 
     @Override
     public String toString() {
-        return mPropertyName + ": " + mKeyframeSet.toString();
+        return mPropertyName + ": " + mKeyframes.toString();
     }
 
     /**
@@ -1059,21 +1082,21 @@
         long mJniSetter;
         private IntProperty mIntProperty;
 
-        IntKeyframeSet mIntKeyframeSet;
+        Keyframes.IntKeyframes mIntKeyframes;
         int mIntAnimatedValue;
 
-        public IntPropertyValuesHolder(String propertyName, IntKeyframeSet keyframeSet) {
+        public IntPropertyValuesHolder(String propertyName, Keyframes.IntKeyframes keyframes) {
             super(propertyName);
             mValueType = int.class;
-            mKeyframeSet = keyframeSet;
-            mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;
+            mKeyframes = keyframes;
+            mIntKeyframes = keyframes;
         }
 
-        public IntPropertyValuesHolder(Property property, IntKeyframeSet keyframeSet) {
+        public IntPropertyValuesHolder(Property property, Keyframes.IntKeyframes keyframes) {
             super(property);
             mValueType = int.class;
-            mKeyframeSet = keyframeSet;
-            mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;
+            mKeyframes = keyframes;
+            mIntKeyframes = keyframes;
             if (property instanceof  IntProperty) {
                 mIntProperty = (IntProperty) mProperty;
             }
@@ -1095,12 +1118,12 @@
         @Override
         public void setIntValues(int... values) {
             super.setIntValues(values);
-            mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;
+            mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
         }
 
         @Override
         void calculateValue(float fraction) {
-            mIntAnimatedValue = mIntKeyframeSet.getIntValue(fraction);
+            mIntAnimatedValue = mIntKeyframes.getIntValue(fraction);
         }
 
         @Override
@@ -1111,7 +1134,7 @@
         @Override
         public IntPropertyValuesHolder clone() {
             IntPropertyValuesHolder newPVH = (IntPropertyValuesHolder) super.clone();
-            newPVH.mIntKeyframeSet = (IntKeyframeSet) newPVH.mKeyframeSet;
+            newPVH.mIntKeyframes = (Keyframes.IntKeyframes) newPVH.mKeyframes;
             return newPVH;
         }
 
@@ -1196,21 +1219,21 @@
         long mJniSetter;
         private FloatProperty mFloatProperty;
 
-        FloatKeyframeSet mFloatKeyframeSet;
+        Keyframes.FloatKeyframes mFloatKeyframes;
         float mFloatAnimatedValue;
 
-        public FloatPropertyValuesHolder(String propertyName, FloatKeyframeSet keyframeSet) {
+        public FloatPropertyValuesHolder(String propertyName, Keyframes.FloatKeyframes keyframes) {
             super(propertyName);
             mValueType = float.class;
-            mKeyframeSet = keyframeSet;
-            mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet;
+            mKeyframes = keyframes;
+            mFloatKeyframes = keyframes;
         }
 
-        public FloatPropertyValuesHolder(Property property, FloatKeyframeSet keyframeSet) {
+        public FloatPropertyValuesHolder(Property property, Keyframes.FloatKeyframes keyframes) {
             super(property);
             mValueType = float.class;
-            mKeyframeSet = keyframeSet;
-            mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet;
+            mKeyframes = keyframes;
+            mFloatKeyframes = keyframes;
             if (property instanceof FloatProperty) {
                 mFloatProperty = (FloatProperty) mProperty;
             }
@@ -1232,12 +1255,12 @@
         @Override
         public void setFloatValues(float... values) {
             super.setFloatValues(values);
-            mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet;
+            mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
         }
 
         @Override
         void calculateValue(float fraction) {
-            mFloatAnimatedValue = mFloatKeyframeSet.getFloatValue(fraction);
+            mFloatAnimatedValue = mFloatKeyframes.getFloatValue(fraction);
         }
 
         @Override
@@ -1248,7 +1271,7 @@
         @Override
         public FloatPropertyValuesHolder clone() {
             FloatPropertyValuesHolder newPVH = (FloatPropertyValuesHolder) super.clone();
-            newPVH.mFloatKeyframeSet = (FloatKeyframeSet) newPVH.mKeyframeSet;
+            newPVH.mFloatKeyframes = (Keyframes.FloatKeyframes) newPVH.mKeyframes;
             return newPVH;
         }
 
@@ -1340,10 +1363,10 @@
         }
 
         public MultiFloatValuesHolder(String propertyName, TypeConverter converter,
-                TypeEvaluator evaluator, KeyframeSet keyframeSet) {
+                TypeEvaluator evaluator, Keyframes keyframes) {
             super(propertyName);
             setConverter(converter);
-            mKeyframeSet = keyframeSet;
+            mKeyframes = keyframes;
             setEvaluator(evaluator);
         }
 
@@ -1443,10 +1466,10 @@
         }
 
         public MultiIntValuesHolder(String propertyName, TypeConverter converter,
-                TypeEvaluator evaluator, KeyframeSet keyframeSet) {
+                TypeEvaluator evaluator, Keyframes keyframes) {
             super(propertyName);
             setConverter(converter);
-            mKeyframeSet = keyframeSet;
+            mKeyframes = keyframes;
             setEvaluator(evaluator);
         }
 
@@ -1532,77 +1555,6 @@
         }
     }
 
-    /* Path interpolation relies on approximating the Path as a series of line segments.
-       The line segments are recursively divided until there is less than 1/2 pixel error
-       between the lines and the curve. Each point of the line segment is converted
-       to a Keyframe and a linear interpolation between Keyframes creates a good approximation
-       of the curve.
-
-       The fraction for each Keyframe is the length along the Path to the point, divided by
-       the total Path length. Two points may have the same fraction in the case of a move
-       command causing a disjoint Path.
-
-       The value for each Keyframe is either the point as a PointF or one of the x or y
-       coordinates as an int or float. In the latter case, two Keyframes are generated for
-       each point that have the same fraction. */
-
-    /**
-     * Returns separate Keyframes arrays for the x and y coordinates along a Path. If
-     * isInt is true, the Keyframes will be IntKeyframes, otherwise they will be FloatKeyframes.
-     * The element at index 0 are the x coordinate Keyframes and element at index 1 are the
-     * y coordinate Keyframes. The returned values can be linearly interpolated and get less
-     * than 1/2 pixel error.
-     */
-    static Keyframe[][] createKeyframes(Path path, boolean isInt) {
-        if (path == null || path.isEmpty()) {
-            throw new IllegalArgumentException("The path must not be null or empty");
-        }
-        float[] pointComponents = path.approximate(0.5f);
-
-        int numPoints = pointComponents.length / 3;
-
-        Keyframe[][] keyframes = new Keyframe[2][];
-        keyframes[0] = new Keyframe[numPoints];
-        keyframes[1] = new Keyframe[numPoints];
-        int componentIndex = 0;
-        for (int i = 0; i < numPoints; i++) {
-            float fraction = pointComponents[componentIndex++];
-            float x = pointComponents[componentIndex++];
-            float y = pointComponents[componentIndex++];
-            if (isInt) {
-                keyframes[0][i] = Keyframe.ofInt(fraction, Math.round(x));
-                keyframes[1][i] = Keyframe.ofInt(fraction, Math.round(y));
-            } else {
-                keyframes[0][i] = Keyframe.ofFloat(fraction, x);
-                keyframes[1][i] = Keyframe.ofFloat(fraction, y);
-            }
-        }
-        return keyframes;
-    }
-
-    /**
-     * Returns PointF Keyframes for a Path. The resulting points can be linearly interpolated
-     * with less than 1/2 pixel in error.
-     */
-    private static Keyframe[] createKeyframes(Path path) {
-        if (path == null || path.isEmpty()) {
-            throw new IllegalArgumentException("The path must not be null or empty");
-        }
-        float[] pointComponents = path.approximate(0.5f);
-
-        int numPoints = pointComponents.length / 3;
-
-        Keyframe[] keyframes = new Keyframe[numPoints];
-        int componentIndex = 0;
-        for (int i = 0; i < numPoints; i++) {
-            float fraction = pointComponents[componentIndex++];
-            float x = pointComponents[componentIndex++];
-            float y = pointComponents[componentIndex++];
-            keyframes[i] = Keyframe.ofObject(fraction, new PointF(x, y));
-        }
-        return keyframes;
-    }
-
     /**
      * Convert from PointF to float[] for multi-float setters along a Path.
      */