/*
* 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.content.Context;
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, 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, 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 an array of PathDataNode here, which is
* fundamentally an array of floating point numbers.
*/
private static class PathDataEvaluator implements TypeEvaluator<PathParser.PathDataNode[]> {
private PathParser.PathDataNode[] mNodeArray;
/**
* Create a PathParser.PathDataNode[] that does not reuse the animated value.
* Care must be taken when using this option because on every evaluation
* a new <code>PathParser.PathDataNode[]</code> will be allocated.
*/
private PathDataEvaluator() {}
/**
* Create a PathDataEvaluator that reuses <code>nodeArray</code> for every evaluate() call.
* Caution must be taken to ensure that the value returned from
* {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or
* used across threads. The value will be modified on each <code>evaluate()</code> call.
*
* @param nodeArray The array to modify and return from <code>evaluate</code>.
*/
public PathDataEvaluator(PathParser.PathDataNode[] nodeArray) {
mNodeArray = nodeArray;
}
@Override
public PathParser.PathDataNode[] evaluate(float fraction,
PathParser.PathDataNode[] startPathData,
PathParser.PathDataNode[] endPathData) {
if (!PathParser.canMorph(startPathData, endPathData)) {
throw new IllegalArgumentException("Can't interpolate between"
+ " two incompatible pathData");
}
if (mNodeArray == null || !PathParser.canMorph(mNodeArray, startPathData)) {
mNodeArray = PathParser.deepCopyNodes(startPathData);
}
for (int i = 0; i < startPathData.length; i++) {
mNodeArray[i].interpolatePathDataNode(startPathData[i],
endPathData[i], fraction);
}
return mNodeArray;
}
}
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.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 (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 == VALUE_TYPE_FLOAT,
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.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString);
PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString);
if (nodesFrom != null) {
if (nodesTo != null) {
anim.setObjectValues(nodesFrom, nodesTo);
if (!PathParser.canMorph(nodesFrom, nodesTo)) {
throw new InflateException(arrayAnimator.getPositionDescription()
+ " Can't morph from " + fromString + " to " + toString);
}
} else {
anim.setObjectValues((Object)nodesFrom);
}
evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom));
} else if (nodesTo != null) {
anim.setObjectValues((Object)nodesTo);
evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo));
}
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,
boolean getFloats, 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 (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 (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.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 int getChangingConfigs(Resources resources, 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);
}
}