/* * 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. *
* 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.
* something 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 ConfigurationBoundResourceCachePathParser.PathDataNode[]
will be allocated.
*/
private PathDataEvaluator() {}
/**
* Create a PathDataEvaluator that reuses nodeArray
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 evaluate()
call.
*
* @param nodeArray The array to modify and return from evaluate
.
*/
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