1086 lines
46 KiB
Java
1086 lines
46 KiB
Java
/*
|
|
* 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));
|
|
}
|
|
int cacheGeneration = animatorCache.getGeneration();
|
|
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, cacheGeneration);
|
|
// 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;
|
|
}
|
|
int cacheGeneration = cache.getGeneration();
|
|
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, cacheGeneration);
|
|
// 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);
|
|
}
|
|
}
|