/* * Copyright (C) 2017 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.support.graphics.drawable; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static java.lang.Math.min; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorSet; import android.animation.Keyframe; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.TypeEvaluator; import android.animation.ValueAnimator; import android.content.Context; 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.graphics.PathMeasure; import android.os.Build; import android.support.annotation.AnimatorRes; import android.support.annotation.RestrictTo; import android.support.v4.content.res.TypedArrayUtils; import android.support.v4.graphics.PathParser; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.util.Xml; import android.view.InflateException; import android.view.animation.Interpolator; 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.)
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public class AnimatorInflaterCompat {
private static final String TAG = "AnimatorInflater";
/**
* These flags are used when parsing AnimatorSet objects
*/
private static final int TOGETHER = 0;
private static final int MAX_NUM_POINTS = 100;
/**
* 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;
/**
* Loads an {@link Animator} object from a context
*
* @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 NotFoundException when the animation cannot be loaded
*/
public static Animator loadAnimator(Context context, @AnimatorRes int id)
throws NotFoundException {
Animator objectAnimator;
// Since AVDC will fall back onto AVD when API is >= 24, therefore, PathParser will need
// to match the accordingly to be able to call into the right setter/ getter for animation.
if (Build.VERSION.SDK_INT >= 24) {
objectAnimator = AnimatorInflater.loadAnimator(context, id);
} else {
objectAnimator = loadAnimator(context, context.getResources(), context.getTheme(), id);
}
return objectAnimator;
}
/**
* Loads an {@link Animator} object from a resource, context is for loading interpolator.
*
* @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 NotFoundException when the animation cannot be loaded
*/
public static Animator loadAnimator(Context context, Resources resources, Theme theme,
@AnimatorRes int id) throws NotFoundException {
return loadAnimator(context, resources, theme, id, 1);
}
/**
* Loads an {@link Animator} object from a resource, context is for loading interpolator.
*/
public static Animator loadAnimator(Context context, Resources resources, Theme theme,
@AnimatorRes int id, float pathErrorScale) throws NotFoundException {
Animator animator;
XmlResourceParser parser = null;
try {
parser = resources.getAnimation(id);
animator = createAnimatorFromXml(context, resources, theme, parser, pathErrorScale);
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();
}
}
/**
* 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
TypeEvaluatorPathParser.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
.
*/
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();
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,
XmlPullParser parser) {
long duration = TypedArrayUtils.getNamedInt(arrayAnimator, parser, "duration",
AndroidResources.STYLEABLE_ANIMATOR_DURATION, 300);
long startDelay = TypedArrayUtils.getNamedInt(arrayAnimator, parser, "startOffset",
AndroidResources.STYLEABLE_ANIMATOR_START_OFFSET, 0);
int valueType = TypedArrayUtils.getNamedInt(arrayAnimator, parser, "valueType",
AndroidResources.STYLEABLE_ANIMATOR_VALUE_TYPE, VALUE_TYPE_UNDEFINED);
// Change to requiring both value from and to, otherwise, throw exception for now.
if (TypedArrayUtils.hasAttribute(parser, "valueFrom")
&& TypedArrayUtils.hasAttribute(parser, "valueTo")) {
if (valueType == VALUE_TYPE_UNDEFINED) {
valueType = inferValueTypeFromValues(arrayAnimator,
AndroidResources.STYLEABLE_ANIMATOR_VALUE_FROM,
AndroidResources.STYLEABLE_ANIMATOR_VALUE_TO);
}
PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType,
AndroidResources.STYLEABLE_ANIMATOR_VALUE_FROM,
AndroidResources.STYLEABLE_ANIMATOR_VALUE_TO, "");
if (pvh != null) {
anim.setValues(pvh);
}
}
anim.setDuration(duration);
anim.setStartDelay(startDelay);
anim.setRepeatCount(TypedArrayUtils.getNamedInt(arrayAnimator, parser, "repeatCount",
AndroidResources.STYLEABLE_ANIMATOR_REPEAT_COUNT, 0));
anim.setRepeatMode(TypedArrayUtils.getNamedInt(arrayAnimator, parser, "repeatMode",
AndroidResources.STYLEABLE_ANIMATOR_REPEAT_MODE, ValueAnimator.RESTART));
if (arrayObjectAnimator != null) {
setupObjectAnimator(anim, arrayObjectAnimator, valueType, pixelSize, parser);
}
}
/**
* Setup ObjectAnimator's property or values from pathData.
*
* @param anim The target Animator which will be updated.
* @param arrayObjectAnimator TypedArray for the ObjectAnimator.
* @param pixelSize The relative pixel size, used to calculate the
*/
private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator,
int valueType, float pixelSize, XmlPullParser parser) {
ObjectAnimator oa = (ObjectAnimator) anim;
String pathData = TypedArrayUtils.getNamedString(arrayObjectAnimator, parser, "pathData",
AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PATH_DATA);
// 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 = TypedArrayUtils.getNamedString(arrayObjectAnimator, parser,
"propertyXName", AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_X_NAME);
String propertyYName = TypedArrayUtils.getNamedString(arrayObjectAnimator, parser,
"propertyYName", AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_Y_NAME);
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);
setupPathMotion(path, oa, 0.5f * pixelSize, propertyXName, propertyYName);
}
} else {
String propertyName =
TypedArrayUtils.getNamedString(arrayObjectAnimator, parser, "propertyName",
AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_NAME);
oa.setPropertyName(propertyName);
}
return;
}
private static void setupPathMotion(Path path, ObjectAnimator oa, float precision,
String propertyXName, String propertyYName) {
// Measure the total length the whole path.
final PathMeasure measureForTotalLength = new PathMeasure(path, false);
float totalLength = 0;
// The sum of the previous contour plus the current one. Using the sum here b/c we want to
// directly substract from it later.
ArrayList