summaryrefslogtreecommitdiff
path: root/android/animation/AnimatorInflater.java
diff options
context:
space:
mode:
Diffstat (limited to 'android/animation/AnimatorInflater.java')
-rw-r--r--android/animation/AnimatorInflater.java1082
1 files changed, 1082 insertions, 0 deletions
diff --git a/android/animation/AnimatorInflater.java b/android/animation/AnimatorInflater.java
new file mode 100644
index 00000000..f69bbfd3
--- /dev/null
+++ b/android/animation/AnimatorInflater.java
@@ -0,0 +1,1082 @@
+/*
+ * Copyright (C) 2010 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.annotation.AnimatorRes;
+import android.annotation.AnyRes;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.ActivityInfo.Config;
+import android.content.res.ConfigurationBoundResourceCache;
+import android.content.res.ConstantState;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.PathParser;
+import android.util.StateSet;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.InflateException;
+import android.view.animation.AnimationUtils;
+import android.view.animation.BaseInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * This class is used to instantiate animator XML files into Animator objects.
+ * <p>
+ * For performance reasons, inflation relies heavily on pre-processing of
+ * XML files that is done at build time. Therefore, it is not currently possible
+ * to use this inflater with an XmlPullParser over a plain XML file at runtime;
+ * it only works with an XmlPullParser returned from a compiled resource (R.
+ * <em>something</em> file.)
+ */
+public class AnimatorInflater {
+ private static final String TAG = "AnimatorInflater";
+ /**
+ * These flags are used when parsing AnimatorSet objects
+ */
+ private static final int TOGETHER = 0;
+ private static final int SEQUENTIALLY = 1;
+
+ /**
+ * Enum values used in XML attributes to indicate the value for mValueType
+ */
+ 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 = 3;
+ private static final int VALUE_TYPE_UNDEFINED = 4;
+
+ private static final boolean DBG_ANIMATOR_INFLATER = false;
+
+ // used to calculate changing configs for resource references
+ private static final TypedValue sTmpTypedValue = new TypedValue();
+
+ /**
+ * Loads an {@link Animator} object from a resource
+ *
+ * @param context Application context used to access resources
+ * @param id The resource id of the animation to load
+ * @return The animator object reference by the specified id
+ * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded
+ */
+ public static Animator loadAnimator(Context context, @AnimatorRes int id)
+ throws NotFoundException {
+ return loadAnimator(context.getResources(), context.getTheme(), id);
+ }
+
+ /**
+ * Loads an {@link Animator} object from a resource
+ *
+ * @param resources The resources
+ * @param theme The theme
+ * @param id The resource id of the animation to load
+ * @return The animator object reference by the specified id
+ * @throws android.content.res.Resources.NotFoundException when the animation cannot be loaded
+ * @hide
+ */
+ public static Animator loadAnimator(Resources resources, Theme theme, int id)
+ throws NotFoundException {
+ return loadAnimator(resources, theme, id, 1);
+ }
+
+ /** @hide */
+ public static Animator loadAnimator(Resources resources, Theme theme, int id,
+ float pathErrorScale) throws NotFoundException {
+ final ConfigurationBoundResourceCache<Animator> animatorCache = resources
+ .getAnimatorCache();
+ Animator animator = animatorCache.getInstance(id, resources, theme);
+ if (animator != null) {
+ if (DBG_ANIMATOR_INFLATER) {
+ Log.d(TAG, "loaded animator from cache, " + resources.getResourceName(id));
+ }
+ return animator;
+ } else if (DBG_ANIMATOR_INFLATER) {
+ Log.d(TAG, "cache miss for animator " + resources.getResourceName(id));
+ }
+ XmlResourceParser parser = null;
+ try {
+ parser = resources.getAnimation(id);
+ animator = createAnimatorFromXml(resources, theme, parser, pathErrorScale);
+ if (animator != null) {
+ animator.appendChangingConfigurations(getChangingConfigs(resources, id));
+ final ConstantState<Animator> constantState = animator.createConstantState();
+ if (constantState != null) {
+ if (DBG_ANIMATOR_INFLATER) {
+ Log.d(TAG, "caching animator for res " + resources.getResourceName(id));
+ }
+ animatorCache.put(id, theme, constantState);
+ // create a new animator so that cached version is never used by the user
+ animator = constantState.newInstance(resources, theme);
+ }
+ }
+ return animator;
+ } catch (XmlPullParserException ex) {
+ Resources.NotFoundException rnf =
+ new Resources.NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ Resources.NotFoundException rnf =
+ new Resources.NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ public static StateListAnimator loadStateListAnimator(Context context, int id)
+ throws NotFoundException {
+ final Resources resources = context.getResources();
+ final ConfigurationBoundResourceCache<StateListAnimator> cache = resources
+ .getStateListAnimatorCache();
+ final Theme theme = context.getTheme();
+ StateListAnimator animator = cache.getInstance(id, resources, theme);
+ if (animator != null) {
+ return animator;
+ }
+ XmlResourceParser parser = null;
+ try {
+ parser = resources.getAnimation(id);
+ animator = createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser));
+ if (animator != null) {
+ animator.appendChangingConfigurations(getChangingConfigs(resources, id));
+ final ConstantState<StateListAnimator> constantState = animator
+ .createConstantState();
+ if (constantState != null) {
+ cache.put(id, theme, constantState);
+ // return a clone so that the animator in constant state is never used.
+ animator = constantState.newInstance(resources, theme);
+ }
+ }
+ return animator;
+ } catch (XmlPullParserException ex) {
+ Resources.NotFoundException rnf =
+ new Resources.NotFoundException(
+ "Can't load state list animator resource ID #0x" +
+ Integer.toHexString(id)
+ );
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ Resources.NotFoundException rnf =
+ new Resources.NotFoundException(
+ "Can't load state list animator resource ID #0x" +
+ Integer.toHexString(id)
+ );
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ }
+
+ private static StateListAnimator createStateListAnimatorFromXml(Context context,
+ XmlPullParser parser, AttributeSet attributeSet)
+ throws IOException, XmlPullParserException {
+ int type;
+ StateListAnimator stateListAnimator = new StateListAnimator();
+
+ while (true) {
+ type = parser.next();
+ switch (type) {
+ case XmlPullParser.END_DOCUMENT:
+ case XmlPullParser.END_TAG:
+ return stateListAnimator;
+
+ case XmlPullParser.START_TAG:
+ // parse item
+ Animator animator = null;
+ if ("item".equals(parser.getName())) {
+ int attributeCount = parser.getAttributeCount();
+ int[] states = new int[attributeCount];
+ int stateIndex = 0;
+ for (int i = 0; i < attributeCount; i++) {
+ int attrName = attributeSet.getAttributeNameResource(i);
+ if (attrName == R.attr.animation) {
+ final int animId = attributeSet.getAttributeResourceValue(i, 0);
+ animator = loadAnimator(context, animId);
+ } else {
+ states[stateIndex++] =
+ attributeSet.getAttributeBooleanValue(i, false) ?
+ attrName : -attrName;
+ }
+ }
+ if (animator == null) {
+ animator = createAnimatorFromXml(context.getResources(),
+ context.getTheme(), parser, 1f);
+ }
+
+ if (animator == null) {
+ throw new Resources.NotFoundException(
+ "animation state item must have a valid animation");
+ }
+ stateListAnimator
+ .addState(StateSet.trimStateSet(states, stateIndex), animator);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * PathDataEvaluator is used to interpolate between two paths which are
+ * represented in the same format but different control points' values.
+ * The path is represented as verbs and points for each of the verbs.
+ */
+ private static class PathDataEvaluator implements TypeEvaluator<PathParser.PathData> {
+ private final PathParser.PathData mPathData = new PathParser.PathData();
+
+ @Override
+ public PathParser.PathData evaluate(float fraction, PathParser.PathData startPathData,
+ PathParser.PathData endPathData) {
+ if (!PathParser.interpolatePathData(mPathData, startPathData, endPathData, fraction)) {
+ throw new IllegalArgumentException("Can't interpolate between"
+ + " two incompatible pathData");
+ }
+ return mPathData;
+ }
+ }
+
+ private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType,
+ int valueFromId, int valueToId, String propertyName) {
+
+ 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;
+
+ if (valueType == VALUE_TYPE_UNDEFINED) {
+ // Check whether it's color type. If not, fall back to default type (i.e. float type)
+ if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
+ valueType = VALUE_TYPE_COLOR;
+ } else {
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ }
+
+ boolean getFloats = (valueType == VALUE_TYPE_FLOAT);
+
+ PropertyValuesHolder returnValue = null;
+
+ if (valueType == VALUE_TYPE_PATH) {
+ String fromString = styledAttributes.getString(valueFromId);
+ String toString = styledAttributes.getString(valueToId);
+ PathParser.PathData nodesFrom = fromString == null
+ ? null : new PathParser.PathData(fromString);
+ PathParser.PathData nodesTo = toString == null
+ ? null : new PathParser.PathData(toString);
+
+ if (nodesFrom != null || nodesTo != null) {
+ if (nodesFrom != null) {
+ TypeEvaluator evaluator = new PathDataEvaluator();
+ 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();
+ returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
+ (Object) nodesTo);
+ }
+ }
+ } else {
+ TypeEvaluator evaluator = null;
+ // Integer and float value types are handled here.
+ if (valueType == VALUE_TYPE_COLOR) {
+ // special case for colors: ignore valueType and get ints
+ 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 (isColorType(fromType)) {
+ 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 (isColorType(toType)) {
+ 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 (isColorType(toType)) {
+ 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.
+ * @param arrayObjectAnimator Incoming typed array for Object Animator's
+ * attributes.
+ * @param pixelSize The relative pixel size, used to calculate the
+ * maximum error for path animations.
+ */
+ private static void parseAnimatorFromTypeArray(ValueAnimator anim,
+ TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) {
+ long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300);
+
+ long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0);
+
+ int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, VALUE_TYPE_UNDEFINED);
+
+ if (valueType == VALUE_TYPE_UNDEFINED) {
+ valueType = inferValueTypeFromValues(arrayAnimator, R.styleable.Animator_valueFrom,
+ R.styleable.Animator_valueTo);
+ }
+ PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType,
+ R.styleable.Animator_valueFrom, R.styleable.Animator_valueTo, "");
+ if (pvh != null) {
+ anim.setValues(pvh);
+ }
+
+ anim.setDuration(duration);
+ anim.setStartDelay(startDelay);
+
+ if (arrayAnimator.hasValue(R.styleable.Animator_repeatCount)) {
+ anim.setRepeatCount(
+ arrayAnimator.getInt(R.styleable.Animator_repeatCount, 0));
+ }
+ if (arrayAnimator.hasValue(R.styleable.Animator_repeatMode)) {
+ anim.setRepeatMode(
+ arrayAnimator.getInt(R.styleable.Animator_repeatMode,
+ ValueAnimator.RESTART));
+ }
+
+ if (arrayObjectAnimator != null) {
+ setupObjectAnimator(anim, arrayObjectAnimator, valueType, pixelSize);
+ }
+ }
+
+ /**
+ * Setup the Animator to achieve path morphing.
+ *
+ * @param anim The target Animator which will be updated.
+ * @param arrayAnimator TypedArray for the ValueAnimator.
+ * @return the PathDataEvaluator.
+ */
+ private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim,
+ TypedArray arrayAnimator) {
+ TypeEvaluator evaluator = null;
+ String fromString = arrayAnimator.getString(R.styleable.Animator_valueFrom);
+ String toString = arrayAnimator.getString(R.styleable.Animator_valueTo);
+ PathParser.PathData pathDataFrom = fromString == null
+ ? null : new PathParser.PathData(fromString);
+ PathParser.PathData pathDataTo = toString == null
+ ? null : new PathParser.PathData(toString);
+
+ if (pathDataFrom != null) {
+ if (pathDataTo != null) {
+ anim.setObjectValues(pathDataFrom, pathDataTo);
+ if (!PathParser.canMorph(pathDataFrom, pathDataTo)) {
+ throw new InflateException(arrayAnimator.getPositionDescription()
+ + " Can't morph from " + fromString + " to " + toString);
+ }
+ } else {
+ anim.setObjectValues((Object)pathDataFrom);
+ }
+ evaluator = new PathDataEvaluator();
+ } else if (pathDataTo != null) {
+ anim.setObjectValues((Object)pathDataTo);
+ evaluator = new PathDataEvaluator();
+ }
+
+ if (DBG_ANIMATOR_INFLATER && evaluator != null) {
+ Log.v(TAG, "create a new PathDataEvaluator here");
+ }
+
+ return evaluator;
+ }
+
+ /**
+ * Setup ObjectAnimator's property or values from pathData.
+ *
+ * @param anim The target Animator which will be updated.
+ * @param arrayObjectAnimator TypedArray for the ObjectAnimator.
+ * @param getFloats True if the value type is float.
+ * @param pixelSize The relative pixel size, used to calculate the
+ * maximum error for path animations.
+ */
+ private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator,
+ int valueType, float pixelSize) {
+ ObjectAnimator oa = (ObjectAnimator) anim;
+ String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData);
+
+ // Path can be involved in an ObjectAnimator in the following 3 ways:
+ // 1) Path morphing: the property to be animated is pathData, and valueFrom and valueTo
+ // are both of pathType. valueType = pathType needs to be explicitly defined.
+ // 2) A property in X or Y dimension can be animated along a path: the property needs to be
+ // defined in propertyXName or propertyYName attribute, the path will be defined in the
+ // pathData attribute. valueFrom and valueTo will not be necessary for this animation.
+ // 3) PathInterpolator can also define a path (in pathData) for its interpolation curve.
+ // Here we are dealing with case 2:
+ if (pathData != null) {
+ String propertyXName =
+ arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyXName);
+ String propertyYName =
+ arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyYName);
+
+ if (valueType == VALUE_TYPE_PATH || valueType == VALUE_TYPE_UNDEFINED) {
+ // When pathData is defined, we are in case #2 mentioned above. ValueType can only
+ // be float type, or int type. Otherwise we fallback to default type.
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ if (propertyXName == null && propertyYName == null) {
+ throw new InflateException(arrayObjectAnimator.getPositionDescription()
+ + " propertyXName or propertyYName is needed for PathData");
+ } else {
+ Path path = PathParser.createPathFromPathData(pathData);
+ float error = 0.5f * pixelSize; // max half a pixel error
+ PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error);
+ Keyframes xKeyframes;
+ Keyframes yKeyframes;
+ if (valueType == VALUE_TYPE_FLOAT) {
+ xKeyframes = keyframeSet.createXFloatKeyframes();
+ yKeyframes = keyframeSet.createYFloatKeyframes();
+ } else {
+ xKeyframes = keyframeSet.createXIntKeyframes();
+ yKeyframes = keyframeSet.createYIntKeyframes();
+ }
+ PropertyValuesHolder x = null;
+ PropertyValuesHolder y = null;
+ if (propertyXName != null) {
+ x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes);
+ }
+ if (propertyYName != null) {
+ y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes);
+ }
+ if (x == null) {
+ oa.setValues(y);
+ } else if (y == null) {
+ oa.setValues(x);
+ } else {
+ oa.setValues(x, y);
+ }
+ }
+ } else {
+ String propertyName =
+ arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyName);
+ oa.setPropertyName(propertyName);
+ }
+ }
+
+ /**
+ * Setup ValueAnimator's values.
+ * This will handle all of the integer, float and color types.
+ *
+ * @param anim The target Animator which will be updated.
+ * @param arrayAnimator TypedArray for the ValueAnimator.
+ * @param getFloats True if the value type is float.
+ * @param hasFrom True if "valueFrom" exists.
+ * @param fromType The type of "valueFrom".
+ * @param hasTo True if "valueTo" exists.
+ * @param toType The type of "valueTo".
+ */
+ private static void setupValues(ValueAnimator anim, TypedArray arrayAnimator,
+ boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType) {
+ int valueFromIndex = R.styleable.Animator_valueFrom;
+ int valueToIndex = R.styleable.Animator_valueTo;
+ if (getFloats) {
+ float valueFrom;
+ float valueTo;
+ if (hasFrom) {
+ if (fromType == TypedValue.TYPE_DIMENSION) {
+ valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f);
+ } else {
+ valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f);
+ }
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
+ } else {
+ valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
+ }
+ anim.setFloatValues(valueFrom, valueTo);
+ } else {
+ anim.setFloatValues(valueFrom);
+ }
+ } else {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
+ } else {
+ valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
+ }
+ anim.setFloatValues(valueTo);
+ }
+ } else {
+ int valueFrom;
+ int valueTo;
+ if (hasFrom) {
+ if (fromType == TypedValue.TYPE_DIMENSION) {
+ valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f);
+ } else if (isColorType(fromType)) {
+ valueFrom = arrayAnimator.getColor(valueFromIndex, 0);
+ } else {
+ valueFrom = arrayAnimator.getInt(valueFromIndex, 0);
+ }
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
+ } else if (isColorType(toType)) {
+ valueTo = arrayAnimator.getColor(valueToIndex, 0);
+ } else {
+ valueTo = arrayAnimator.getInt(valueToIndex, 0);
+ }
+ anim.setIntValues(valueFrom, valueTo);
+ } else {
+ anim.setIntValues(valueFrom);
+ }
+ } else {
+ if (hasTo) {
+ if (toType == TypedValue.TYPE_DIMENSION) {
+ valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
+ } else if (isColorType(toType)) {
+ valueTo = arrayAnimator.getColor(valueToIndex, 0);
+ } else {
+ valueTo = arrayAnimator.getInt(valueToIndex, 0);
+ }
+ anim.setIntValues(valueTo);
+ }
+ }
+ }
+ }
+
+ private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
+ float pixelSize)
+ throws XmlPullParserException, IOException {
+ return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0,
+ pixelSize);
+ }
+
+ private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
+ AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)
+ throws XmlPullParserException, IOException {
+ Animator anim = null;
+ ArrayList<Animator> childAnims = null;
+
+ // Make sure we are on a start tag.
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+ boolean gotValues = false;
+
+ if (name.equals("objectAnimator")) {
+ anim = loadObjectAnimator(res, theme, attrs, pixelSize);
+ } else if (name.equals("animator")) {
+ anim = loadAnimator(res, theme, attrs, null, pixelSize);
+ } else if (name.equals("set")) {
+ anim = new AnimatorSet();
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.AnimatorSet, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.AnimatorSet);
+ }
+ anim.appendChangingConfigurations(a.getChangingConfigurations());
+ int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER);
+ 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 && !gotValues) {
+ if (childAnims == null) {
+ childAnims = new ArrayList<Animator>();
+ }
+ childAnims.add(anim);
+ }
+ }
+ if (parent != null && childAnims != null) {
+ Animator[] animsArray = new Animator[childAnims.size()];
+ int index = 0;
+ for (Animator a : childAnims) {
+ animsArray[index++] = a;
+ }
+ if (sequenceOrdering == TOGETHER) {
+ parent.playTogether(animsArray);
+ } else {
+ parent.playSequentially(animsArray);
+ }
+ }
+ 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_UNDEFINED);
+
+ 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;
+ }
+
+ // When no value type is provided in keyframe, we need to infer the type from the value. i.e.
+ // if value is defined in the style of a color value, then the color type is returned.
+ // Otherwise, default float type is returned.
+ private static int inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs) {
+ int valueType;
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.Keyframe);
+ }
+
+ TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value);
+ boolean hasValue = (keyframeValue != null);
+ // When no value type is provided, check whether it's a color type first.
+ // If not, fall back to default value type (i.e. float type).
+ if (hasValue && isColorType(keyframeValue.type)) {
+ valueType = VALUE_TYPE_COLOR;
+ } else {
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ a.recycle();
+ return valueType;
+ }
+
+ private static int inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId,
+ int valueToId) {
+ 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;
+
+ int valueType;
+ // Check whether it's color type. If not, fall back to default type (i.e. float type)
+ if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) {
+ valueType = VALUE_TYPE_COLOR;
+ } else {
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ return valueType;
+ }
+
+ 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"));
+ }
+ }
+
+ // Load property values holder if there are keyframes defined in it. Otherwise return 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")) {
+ if (valueType == VALUE_TYPE_UNDEFINED) {
+ valueType = inferValueTypeOfKeyframe(res, theme, Xml.asAttributeSet(parser));
+ }
+ 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);
+
+ TypedValue keyframeValue = a.peekValue(R.styleable.Keyframe_value);
+ boolean hasValue = (keyframeValue != null);
+ if (valueType == VALUE_TYPE_UNDEFINED) {
+ // When no value type is provided, check whether it's a color type first.
+ // If not, fall back to default value type (i.e. float type).
+ if (hasValue && isColorType(keyframeValue.type)) {
+ valueType = VALUE_TYPE_COLOR;
+ } else {
+ valueType = VALUE_TYPE_FLOAT;
+ }
+ }
+
+ 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);
+ }
+
+ final int resID = a.getResourceId(R.styleable.Keyframe_interpolator, 0);
+ if (resID > 0) {
+ final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
+ keyframe.setInterpolator(interpolator);
+ }
+ a.recycle();
+
+ return keyframe;
+ }
+
+ private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs,
+ float pathErrorScale) throws NotFoundException {
+ ObjectAnimator anim = new ObjectAnimator();
+
+ loadAnimator(res, theme, attrs, anim, pathErrorScale);
+
+ return anim;
+ }
+
+ /**
+ * Creates a new animation whose parameters come from the specified context
+ * and attributes set.
+ *
+ * @param res The resources
+ * @param attrs The set of attributes holding the animation parameters
+ * @param anim Null if this is a ValueAnimator, otherwise this is an
+ * ObjectAnimator
+ */
+ private static ValueAnimator loadAnimator(Resources res, Theme theme,
+ AttributeSet attrs, ValueAnimator anim, float pathErrorScale)
+ throws NotFoundException {
+ TypedArray arrayAnimator = null;
+ TypedArray arrayObjectAnimator = null;
+
+ if (theme != null) {
+ arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0);
+ } else {
+ arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator);
+ }
+
+ // If anim is not null, then it is an object animator.
+ if (anim != null) {
+ if (theme != null) {
+ arrayObjectAnimator = theme.obtainStyledAttributes(attrs,
+ R.styleable.PropertyAnimator, 0, 0);
+ } else {
+ arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator);
+ }
+ anim.appendChangingConfigurations(arrayObjectAnimator.getChangingConfigurations());
+ }
+
+ if (anim == null) {
+ anim = new ValueAnimator();
+ }
+ anim.appendChangingConfigurations(arrayAnimator.getChangingConfigurations());
+
+ parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale);
+
+ final int resID = arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0);
+ if (resID > 0) {
+ final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
+ if (interpolator instanceof BaseInterpolator) {
+ anim.appendChangingConfigurations(
+ ((BaseInterpolator) interpolator).getChangingConfiguration());
+ }
+ anim.setInterpolator(interpolator);
+ }
+
+ arrayAnimator.recycle();
+ if (arrayObjectAnimator != null) {
+ arrayObjectAnimator.recycle();
+ }
+ return anim;
+ }
+
+ private static @Config int getChangingConfigs(@NonNull Resources resources, @AnyRes int id) {
+ synchronized (sTmpTypedValue) {
+ resources.getValue(id, sTmpTypedValue, true);
+ return sTmpTypedValue.changingConfigurations;
+ }
+ }
+
+ private static boolean isColorType(int type) {
+ return (type >= TypedValue.TYPE_FIRST_COLOR_INT) && (type <= TypedValue.TYPE_LAST_COLOR_INT);
+ }
+}