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" />