Add resource attributes for Keyframes and PropertyValuesHolders

Issue #17939329 Expose multi-property and multi-keyframe capabilities in animation resources

Change-Id: I14822ced47665fa6cde4996f74d3078da2ada38a
diff --git a/api/current.txt b/api/current.txt
index c1d9383..bf4d52c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -593,6 +593,7 @@
     field public static final int format = 16843013; // 0x1010105
     field public static final int format12Hour = 16843722; // 0x10103ca
     field public static final int format24Hour = 16843723; // 0x10103cb
+    field public static final int fraction = 16843992; // 0x10104d8
     field public static final int fragment = 16843491; // 0x10102e3
     field public static final int fragmentAllowEnterTransitionOverlap = 16843976; // 0x10104c8
     field public static final int fragmentAllowReturnTransitionOverlap = 16843977; // 0x10104c9
diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java
index 688d7e4..6ef3da8 100644
--- a/core/java/android/animation/AnimatorInflater.java
+++ b/core/java/android/animation/AnimatorInflater.java
@@ -42,6 +42,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * This class is used to instantiate animator XML files into Animator objects.
@@ -66,8 +67,7 @@
     private static final int VALUE_TYPE_FLOAT       = 0;
     private static final int VALUE_TYPE_INT         = 1;
     private static final int VALUE_TYPE_PATH        = 2;
-    private static final int VALUE_TYPE_COLOR       = 4;
-    private static final int VALUE_TYPE_CUSTOM      = 5;
+    private static final int VALUE_TYPE_COLOR       = 3;
 
     private static final boolean DBG_ANIMATOR_INFLATER = false;
 
@@ -296,6 +296,134 @@
         }
     }
 
+    private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType,
+            int valueFromId, int valueToId, String propertyName) {
+
+        boolean getFloats = (valueType == VALUE_TYPE_FLOAT);
+
+        TypedValue tvFrom = styledAttributes.peekValue(valueFromId);
+        boolean hasFrom = (tvFrom != null);
+        int fromType = hasFrom ? tvFrom.type : 0;
+        TypedValue tvTo = styledAttributes.peekValue(valueToId);
+        boolean hasTo = (tvTo != null);
+        int toType = hasTo ? tvTo.type : 0;
+
+        PropertyValuesHolder returnValue = null;
+
+        if (valueType == VALUE_TYPE_PATH) {
+            String fromString = styledAttributes.getString(valueFromId);
+            String toString = styledAttributes.getString(valueToId);
+            PathParser.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString);
+            PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString);
+
+            if (nodesFrom != null || nodesTo != null) {
+                if (nodesFrom != null) {
+                    TypeEvaluator evaluator =
+                            new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom));
+                    if (nodesTo != null) {
+                        if (!PathParser.canMorph(nodesFrom, nodesTo)) {
+                            throw new InflateException(" Can't morph from " + fromString + " to " +
+                                    toString);
+                        }
+                        returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
+                                nodesFrom, nodesTo);
+                    } else {
+                        returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
+                                (Object) nodesFrom);
+                    }
+                } else if (nodesTo != null) {
+                    TypeEvaluator evaluator =
+                            new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo));
+                    returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
+                            (Object) nodesTo);
+                }
+            }
+        } else {
+            TypeEvaluator evaluator = null;
+            // Integer and float value types are handled here.
+            if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+                    (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) ||
+                    (hasTo && (toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+                            (toType <= TypedValue.TYPE_LAST_COLOR_INT))) {
+                // special case for colors: ignore valueType and get ints
+                getFloats = false;
+                evaluator = ArgbEvaluator.getInstance();
+            }
+            if (getFloats) {
+                float valueFrom;
+                float valueTo;
+                if (hasFrom) {
+                    if (fromType == TypedValue.TYPE_DIMENSION) {
+                        valueFrom = styledAttributes.getDimension(valueFromId, 0f);
+                    } else {
+                        valueFrom = styledAttributes.getFloat(valueFromId, 0f);
+                    }
+                    if (hasTo) {
+                        if (toType == TypedValue.TYPE_DIMENSION) {
+                            valueTo = styledAttributes.getDimension(valueToId, 0f);
+                        } else {
+                            valueTo = styledAttributes.getFloat(valueToId, 0f);
+                        }
+                        returnValue = PropertyValuesHolder.ofFloat(propertyName,
+                                valueFrom, valueTo);
+                    } else {
+                        returnValue = PropertyValuesHolder.ofFloat(propertyName, valueFrom);
+                    }
+                } else {
+                    if (toType == TypedValue.TYPE_DIMENSION) {
+                        valueTo = styledAttributes.getDimension(valueToId, 0f);
+                    } else {
+                        valueTo = styledAttributes.getFloat(valueToId, 0f);
+                    }
+                    returnValue = PropertyValuesHolder.ofFloat(propertyName, valueTo);
+                }
+            } else {
+                int valueFrom;
+                int valueTo;
+                if (hasFrom) {
+                    if (fromType == TypedValue.TYPE_DIMENSION) {
+                        valueFrom = (int) styledAttributes.getDimension(valueFromId, 0f);
+                    } else if ((fromType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+                            (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) {
+                        valueFrom = styledAttributes.getColor(valueFromId, 0);
+                    } else {
+                        valueFrom = styledAttributes.getInt(valueFromId, 0);
+                    }
+                    if (hasTo) {
+                        if (toType == TypedValue.TYPE_DIMENSION) {
+                            valueTo = (int) styledAttributes.getDimension(valueToId, 0f);
+                        } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+                                (toType <= TypedValue.TYPE_LAST_COLOR_INT)) {
+                            valueTo = styledAttributes.getColor(valueToId, 0);
+                        } else {
+                            valueTo = styledAttributes.getInt(valueToId, 0);
+                        }
+                        returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom, valueTo);
+                    } else {
+                        returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom);
+                    }
+                } else {
+                    if (hasTo) {
+                        if (toType == TypedValue.TYPE_DIMENSION) {
+                            valueTo = (int) styledAttributes.getDimension(valueToId, 0f);
+                        } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
+                                (toType <= TypedValue.TYPE_LAST_COLOR_INT)) {
+                            valueTo = styledAttributes.getColor(valueToId, 0);
+                        } else {
+                            valueTo = styledAttributes.getInt(valueToId, 0);
+                        }
+                        returnValue = PropertyValuesHolder.ofInt(propertyName, valueTo);
+                    }
+                }
+            }
+            if (returnValue != null && evaluator != null) {
+                returnValue.setEvaluator(evaluator);
+            }
+        }
+
+        return returnValue;
+    }
+
     /**
      * @param anim The animator, must not be null
      * @param arrayAnimator Incoming typed array for Animator's attributes.
@@ -310,35 +438,12 @@
 
         long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0);
 
-        int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType,
-                VALUE_TYPE_FLOAT);
+        int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, VALUE_TYPE_FLOAT);
 
-        TypeEvaluator evaluator = null;
-
-        boolean getFloats = (valueType == VALUE_TYPE_FLOAT);
-
-        TypedValue tvFrom = arrayAnimator.peekValue(R.styleable.Animator_valueFrom);
-        boolean hasFrom = (tvFrom != null);
-        int fromType = hasFrom ? tvFrom.type : 0;
-        TypedValue tvTo = arrayAnimator.peekValue(R.styleable.Animator_valueTo);
-        boolean hasTo = (tvTo != null);
-        int toType = hasTo ? tvTo.type : 0;
-
-        // TODO: Further clean up this part of code into 4 types : path, color,
-        // integer and float.
-        if (valueType == VALUE_TYPE_PATH) {
-            evaluator = setupAnimatorForPath(anim, arrayAnimator);
-        } else {
-            // Integer and float value types are handled here.
-            if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
-                    (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) ||
-                    (hasTo && (toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
-                            (toType <= TypedValue.TYPE_LAST_COLOR_INT))) {
-                // special case for colors: ignore valueType and get ints
-                getFloats = false;
-                evaluator = ArgbEvaluator.getInstance();
-            }
-            setupValues(anim, arrayAnimator, getFloats, hasFrom, fromType, hasTo, toType);
+        PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType,
+                R.styleable.Animator_valueFrom, R.styleable.Animator_valueTo, "");
+        if (pvh != null) {
+            anim.setValues(pvh);
         }
 
         anim.setDuration(duration);
@@ -353,12 +458,10 @@
                     arrayAnimator.getInt(R.styleable.Animator_repeatMode,
                             ValueAnimator.RESTART));
         }
-        if (evaluator != null) {
-            anim.setEvaluator(evaluator);
-        }
 
         if (arrayObjectAnimator != null) {
-            setupObjectAnimator(anim, arrayObjectAnimator, getFloats, pixelSize);
+            setupObjectAnimator(anim, arrayObjectAnimator, valueType == VALUE_TYPE_FLOAT,
+                    pixelSize);
         }
     }
 
@@ -570,6 +673,7 @@
             }
 
             String name = parser.getName();
+            boolean gotValues = false;
 
             if (name.equals("objectAnimator")) {
                 anim = loadObjectAnimator(res, theme, attrs, pixelSize);
@@ -588,11 +692,18 @@
                 createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering,
                         pixelSize);
                 a.recycle();
+            } else if (name.equals("propertyValuesHolder")) {
+                PropertyValuesHolder[] values = loadValues(res, theme, parser,
+                        Xml.asAttributeSet(parser));
+                if (values != null && anim != null && (anim instanceof ValueAnimator)) {
+                    ((ValueAnimator) anim).setValues(values);
+                }
+                gotValues = true;
             } else {
                 throw new RuntimeException("Unknown animator name: " + parser.getName());
             }
 
-            if (parent != null) {
+            if (parent != null && !gotValues) {
                 if (childAnims == null) {
                     childAnims = new ArrayList<Animator>();
                 }
@@ -612,7 +723,233 @@
             }
         }
         return anim;
+    }
 
+    private static PropertyValuesHolder[] loadValues(Resources res, Theme theme,
+            XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
+        ArrayList<PropertyValuesHolder> values = null;
+
+        int type;
+        while ((type = parser.getEventType()) != XmlPullParser.END_TAG &&
+                type != XmlPullParser.END_DOCUMENT) {
+
+            if (type != XmlPullParser.START_TAG) {
+                parser.next();
+                continue;
+            }
+
+            String name = parser.getName();
+
+            if (name.equals("propertyValuesHolder")) {
+                TypedArray a;
+                if (theme != null) {
+                    a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyValuesHolder, 0, 0);
+                } else {
+                    a = res.obtainAttributes(attrs, R.styleable.PropertyValuesHolder);
+                }
+                String propertyName = a.getString(R.styleable.PropertyValuesHolder_propertyName);
+                int valueType = a.getInt(R.styleable.PropertyValuesHolder_valueType,
+                        VALUE_TYPE_FLOAT);
+                PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType);
+                if (pvh == null) {
+                    pvh = getPVH(a, valueType,
+                            R.styleable.PropertyValuesHolder_valueFrom,
+                            R.styleable.PropertyValuesHolder_valueTo, propertyName);
+                }
+                if (pvh != null) {
+                    if (values == null) {
+                        values = new ArrayList<PropertyValuesHolder>();
+                    }
+                    values.add(pvh);
+                }
+                a.recycle();
+            }
+
+            parser.next();
+        }
+
+        PropertyValuesHolder[] valuesArray = null;
+        if (values != null) {
+            int count = values.size();
+            valuesArray = new PropertyValuesHolder[count];
+            for (int i = 0; i < count; ++i) {
+                valuesArray[i] = values.get(i);
+            }
+        }
+        return valuesArray;
+    }
+
+    private static void dumpKeyframes(Object[] keyframes, String header) {
+        if (keyframes == null || keyframes.length == 0) {
+            return;
+        }
+        Log.d(TAG, header);
+        int count = keyframes.length;
+        for (int i = 0; i < count; ++i) {
+            Keyframe keyframe = (Keyframe) keyframes[i];
+            Log.d(TAG, "Keyframe " + i + ": fraction " +
+                    (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " +
+                    ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null"));
+        }
+    }
+
+    private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser,
+            String propertyName, int valueType)
+            throws XmlPullParserException, IOException {
+
+        PropertyValuesHolder value = null;
+        ArrayList<Keyframe> keyframes = null;
+
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_TAG &&
+                type != XmlPullParser.END_DOCUMENT) {
+            String name = parser.getName();
+            if (name.equals("keyframe")) {
+                Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType);
+                if (keyframe != null) {
+                    if (keyframes == null) {
+                        keyframes = new ArrayList<Keyframe>();
+                    }
+                    keyframes.add(keyframe);
+                }
+                parser.next();
+            }
+        }
+
+        int count;
+        if (keyframes != null && (count = keyframes.size()) > 0) {
+            // make sure we have keyframes at 0 and 1
+            // If we have keyframes with set fractions, add keyframes at start/end
+            // appropriately. If start/end have no set fractions:
+            // if there's only one keyframe, set its fraction to 1 and add one at 0
+            // if >1 keyframe, set the last fraction to 1, the first fraction to 0
+            Keyframe firstKeyframe = keyframes.get(0);
+            Keyframe lastKeyframe = keyframes.get(count - 1);
+            float endFraction = lastKeyframe.getFraction();
+            if (endFraction < 1) {
+                if (endFraction < 0) {
+                    lastKeyframe.setFraction(1);
+                } else {
+                    keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1));
+                    ++count;
+                }
+            }
+            float startFraction = firstKeyframe.getFraction();
+            if (startFraction != 0) {
+                if (startFraction < 0) {
+                    firstKeyframe.setFraction(0);
+                } else {
+                    keyframes.add(0, createNewKeyframe(firstKeyframe, 0));
+                    ++count;
+                }
+            }
+            Keyframe[] keyframeArray = new Keyframe[count];
+            keyframes.toArray(keyframeArray);
+            for (int i = 0; i < count; ++i) {
+                Keyframe keyframe = keyframeArray[i];
+                if (keyframe.getFraction() < 0) {
+                    if (i == 0) {
+                        keyframe.setFraction(0);
+                    } else if (i == count - 1) {
+                        keyframe.setFraction(1);
+                    } else {
+                        // figure out the start/end parameters of the current gap
+                        // in fractions and distribute the gap among those keyframes
+                        int startIndex = i;
+                        int endIndex = i;
+                        for (int j = startIndex + 1; j < count - 1; ++j) {
+                            if (keyframeArray[j].getFraction() >= 0) {
+                                break;
+                            }
+                            endIndex = j;
+                        }
+                        float gap = keyframeArray[endIndex + 1].getFraction() -
+                                keyframeArray[startIndex - 1].getFraction();
+                        distributeKeyframes(keyframeArray, gap, startIndex, endIndex);
+                    }
+                }
+            }
+            value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray);
+            if (valueType == VALUE_TYPE_COLOR) {
+                value.setEvaluator(ArgbEvaluator.getInstance());
+            }
+        }
+
+        return value;
+    }
+
+    private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) {
+        return sampleKeyframe.getType() == float.class ?
+                            Keyframe.ofFloat(fraction) :
+                            (sampleKeyframe.getType() == int.class) ?
+                                    Keyframe.ofInt(fraction) :
+                                    Keyframe.ofObject(fraction);
+    }
+
+    /**
+     * Utility function to set fractions on keyframes to cover a gap in which the
+     * fractions are not currently set. Keyframe fractions will be distributed evenly
+     * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap
+     * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the
+     * keyframe before startIndex.
+     * Assumptions:
+     * - First and last keyframe fractions (bounding this spread) are already set. So,
+     * for example, if no fractions are set, we will already set first and last keyframe
+     * fraction values to 0 and 1.
+     * - startIndex must be >0 (which follows from first assumption).
+     * - endIndex must be >= startIndex.
+     *
+     * @param keyframes the array of keyframes
+     * @param gap The total gap we need to distribute
+     * @param startIndex The index of the first keyframe whose fraction must be set
+     * @param endIndex The index of the last keyframe whose fraction must be set
+     */
+    private static void distributeKeyframes(Keyframe[] keyframes, float gap,
+            int startIndex, int endIndex) {
+        int count = endIndex - startIndex + 2;
+        float increment = gap / count;
+        for (int i = startIndex; i <= endIndex; ++i) {
+            keyframes[i].setFraction(keyframes[i-1].getFraction() + increment);
+        }
+    }
+
+    private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs,
+            int valueType)
+            throws XmlPullParserException, IOException {
+
+        TypedArray a;
+        if (theme != null) {
+            a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0);
+        } else {
+            a = res.obtainAttributes(attrs, R.styleable.Keyframe);
+        }
+
+        Keyframe keyframe = null;
+
+        float fraction = a.getFloat(R.styleable.Keyframe_fraction, -1);
+
+        boolean hasValue = a.peekValue(R.styleable.Keyframe_value) != null;
+
+        if (hasValue) {
+            switch (valueType) {
+                case VALUE_TYPE_FLOAT:
+                    float value = a.getFloat(R.styleable.Keyframe_value, 0);
+                    keyframe = Keyframe.ofFloat(fraction, value);
+                    break;
+                case VALUE_TYPE_COLOR:
+                case VALUE_TYPE_INT:
+                    int intValue = a.getInt(R.styleable.Keyframe_value, 0);
+                    keyframe = Keyframe.ofInt(fraction, intValue);
+                    break;
+            }
+        } else {
+            keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) :
+                    Keyframe.ofInt(fraction);
+        }
+
+        a.recycle();
+
+        return keyframe;
     }
 
     private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs,
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 92762c3..53d5237 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -972,6 +972,18 @@
         }
     }
 
+    @Override
+    public String toString() {
+        String returnVal = "AnimatorSet@" + Integer.toHexString(hashCode()) + "{";
+        boolean prevNeedsSort = mNeedsSort;
+        sortNodes();
+        mNeedsSort = prevNeedsSort;
+        for (Node node : mSortedNodes) {
+            returnVal += "\n    " + node.animation.toString();
+        }
+        return returnVal + "\n}";
+    }
+
     /**
      * Dependency holds information about the node that some other node is
      * dependent upon and the nature of that dependency.
diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java
index 59daaab..71855da 100644
--- a/core/java/android/animation/ObjectAnimator.java
+++ b/core/java/android/animation/ObjectAnimator.java
@@ -33,6 +33,27 @@
  * are then determined internally and the animation will call these functions as necessary to
  * animate the property.
  *
+ * <p>Animators can be created from either code or resource files, as shown here:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/object_animator.xml ObjectAnimatorResources}
+ *
+ * <p>When using resource files, it is possible to use {@link PropertyValuesHolder} and
+ * {@link Keyframe} to create more complex animations. Using PropertyValuesHolders
+ * allows animators to animate several properties in parallel, as shown in this sample:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh.xml
+ * PropertyValuesHolderResources}
+ *
+ * <p>Using Keyframes allows animations to follow more complex paths from the start
+ * to the end values. Note that you can specify explicit fractional values (from 0 to 1) for
+ * each keyframe to determine when, in the overall duration, the animation should arrive at that
+ * value. Alternatively, you can leave the fractions off and the keyframes will be equally
+ * distributed within the total duration. Also, a keyframe with no value will derive its value
+ * from the target object when the animator starts, just like animators with only one
+ * value specified.</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh_kf.xml KeyframeResources}
+ *
  * <div class="special reference">
  * <h3>Developer Guides</h3>
  * <p>For more information about animating with {@code ObjectAnimator}, read the
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 8b70ae6..e4becf5 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -17,6 +17,7 @@
 package android.animation;
 
 import android.content.res.ConfigurationBoundResourceCache;
+import android.os.Debug;
 import android.os.Looper;
 import android.os.Trace;
 import android.util.AndroidRuntimeException;
@@ -40,6 +41,21 @@
  * out of an animation. This behavior can be changed by calling
  * {@link ValueAnimator#setInterpolator(TimeInterpolator)}.</p>
  *
+ * <p>Animators can be created from either code or resource files. Here is an example
+ * of a ValueAnimator resource file:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/animator.xml ValueAnimatorResources}
+ *
+ * <p>It is also possible to use a combination of {@link PropertyValuesHolder} and
+ * {@link Keyframe} resource tags to create a multi-step animation.
+ * Note that you can specify explicit fractional values (from 0 to 1) for
+ * each keyframe to determine when, in the overall duration, the animation should arrive at that
+ * value. Alternatively, you can leave the fractions off and the keyframes will be equally
+ * distributed within the total duration:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/anim/value_animator_pvh_kf.xml
+ * ValueAnimatorKeyframeResources}
+ *
  * <div class="special reference">
  * <h3>Developer Guides</h3>
  * <p>For more information about animating with {@code ValueAnimator}, read the
diff --git a/core/java/android/util/AttributeSet.java b/core/java/android/util/AttributeSet.java
index 74942ba..eb8c168 100644
--- a/core/java/android/util/AttributeSet.java
+++ b/core/java/android/util/AttributeSet.java
@@ -39,7 +39,7 @@
  * is more useful in conjunction with compiled XML resources:
  * 
  * <pre>
- * XmlPullParser parser = resources.getXml(myResouce);
+ * XmlPullParser parser = resources.getXml(myResource);
  * AttributeSet attributes = Xml.asAttributeSet(parser);</pre>
  * 
  * <p>The implementation returned here, unlike using
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 52cf8c1..4b449b4 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5854,19 +5854,34 @@
         <attr name="valueTo" format="float|integer|color|dimension|string"/>
         <!-- The type of valueFrom and valueTo. -->
         <attr name="valueType">
-            <!-- valueFrom and valueTo are floats. This is the default value is valueType is
-                 unspecified. Note that if either valueFrom or valueTo represent colors
+            <!-- The given values are floats. This is the default value if valueType is
+                 unspecified. Note that if any value attribute has a color value
                  (beginning with "#"), then this attribute is ignored and the color values are
                  interpreted as integers. -->
             <enum name="floatType" value="0" />
-            <!-- valueFrom and valueTo are integers. -->
+            <!-- values are integers. -->
             <enum name="intType"   value="1" />
-            <!-- valueFrom and valueTo are paths defined as strings.
+            <!-- values are paths defined as strings.
                  This type is used for path morphing in AnimatedVectorDrawable. -->
             <enum name="pathType"   value="2" />
+            <!-- values are colors, which are integers starting with "#". -->
+            <enum name="colorType"   value="3" />
         </attr>
     </declare-styleable>
 
+    <declare-styleable name="PropertyValuesHolder">
+        <attr name="valueType" />
+        <attr name="propertyName" />
+        <attr name="valueFrom" />
+        <attr name="valueTo" />
+    </declare-styleable>
+
+    <declare-styleable name="Keyframe">
+        <attr name="valueType" />
+        <attr name="value" />
+        <attr name="fraction" format="float" />
+    </declare-styleable>
+
     <!-- ========================== -->
     <!-- ObjectAnimator class attributes -->
     <!-- ========================== -->
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index dbf2b4d..9f49b08 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2596,6 +2596,7 @@
     <public type="attr" name="revisionCode" />
     <public type="attr" name="drawableTint" />
     <public type="attr" name="drawableTintMode" />
+    <public type="attr" name="fraction" />
 
     <public type="style" name="Theme.DeviceDefault.Dialog.Alert" />
     <public type="style" name="Theme.DeviceDefault.Light.Dialog.Alert" />