diff options
Diffstat (limited to 'core')
| -rw-r--r-- | core/java/android/animation/AnimatorInflater.java | 266 | ||||
| -rw-r--r-- | core/java/android/util/PathParser.java | 83 | ||||
| -rw-r--r-- | core/res/res/values/attrs.xml | 3 |
3 files changed, 282 insertions, 70 deletions
diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index 39fcf73..0f5e954 100644 --- a/core/java/android/animation/AnimatorInflater.java +++ b/core/java/android/animation/AnimatorInflater.java @@ -23,10 +23,12 @@ 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 com.android.internal.R; @@ -47,7 +49,7 @@ import java.util.ArrayList; * <em>something</em> file.) */ public class AnimatorInflater { - + private static final String TAG = "AnimatorInflater"; /** * These flags are used when parsing AnimatorSet objects */ @@ -59,9 +61,12 @@ public class AnimatorInflater { */ 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 = 4; private static final int VALUE_TYPE_CUSTOM = 5; + private static final boolean DBG_ANIMATOR_INFLATER = false; + /** * Loads an {@link Animator} object from a resource * @@ -189,6 +194,56 @@ public class AnimatorInflater { } /** + * 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; + } + } + + /** * @param anim Null if this is a ValueAnimator, otherwise this is an * ObjectAnimator * @param arrayAnimator Incoming typed array for Animator's attributes. @@ -209,27 +264,157 @@ public class AnimatorInflater { } TypeEvaluator evaluator = null; - int valueFromIndex = R.styleable.Animator_valueFrom; - int valueToIndex = R.styleable.Animator_valueTo; boolean getFloats = (valueType == VALUE_TYPE_FLOAT); - TypedValue tvFrom = arrayAnimator.peekValue(valueFromIndex); + TypedValue tvFrom = arrayAnimator.peekValue(R.styleable.Animator_valueFrom); boolean hasFrom = (tvFrom != null); int fromType = hasFrom ? tvFrom.type : 0; - TypedValue tvTo = arrayAnimator.peekValue(valueToIndex); + TypedValue tvTo = arrayAnimator.peekValue(R.styleable.Animator_valueTo); boolean hasTo = (tvTo != null); int toType = hasTo ? tvTo.type : 0; - if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && - (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) || - (hasTo && (toType >= TypedValue.TYPE_FIRST_COLOR_INT) && - (toType <= TypedValue.TYPE_LAST_COLOR_INT))) { - // special case for colors: ignore valueType and get ints - getFloats = false; - evaluator = ArgbEvaluator.getInstance(); + // TODO: Further clean up this part of code into 4 types : path, color, + // integer and float. + if (valueType == VALUE_TYPE_PATH) { + evaluator = setupAnimatorForPath(anim, arrayAnimator); + } else { + // Integer and float value types are handled here. + if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) || + (hasTo && (toType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (toType <= TypedValue.TYPE_LAST_COLOR_INT))) { + // special case for colors: ignore valueType and get ints + getFloats = false; + evaluator = ArgbEvaluator.getInstance(); + } + setupValues(anim, arrayAnimator, getFloats, hasFrom, fromType, hasTo, toType); } + 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 (evaluator != null) { + anim.setEvaluator(evaluator); + } + + if (arrayObjectAnimator != null) { + setupObjectAnimator(anim, arrayObjectAnimator, getFloats); + } + } + + /** + * 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. + */ + private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator, + boolean getFloats) { + ObjectAnimator oa = (ObjectAnimator) anim; + String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData); + + // Note that if there is a pathData defined in the Object Animator, + // valueFrom / valueTo will be ignored. + 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); + Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, !getFloats); + PropertyValuesHolder x = null; + PropertyValuesHolder y = null; + if (propertyXName != null) { + x = PropertyValuesHolder.ofKeyframe(propertyXName, keyframes[0]); + } + if (propertyYName != null) { + y = PropertyValuesHolder.ofKeyframe(propertyYName, keyframes[1]); + } + 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; @@ -296,63 +481,6 @@ public class AnimatorInflater { } } } - - 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 (evaluator != null) { - anim.setEvaluator(evaluator); - } - - if (arrayObjectAnimator != null) { - ObjectAnimator oa = (ObjectAnimator) anim; - String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData); - - // Note that if there is a pathData defined in the Object Animator, - // valueFrom / valueTo will be overwritten by the pathData. - 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 IllegalArgumentException("propertyXName or propertyYName" - + " is needed for PathData in Object Animator"); - } else { - Path path = PathParser.createPathFromPathData(pathData); - Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, !getFloats); - PropertyValuesHolder x = null; - PropertyValuesHolder y = null; - if (propertyXName != null) { - x = PropertyValuesHolder.ofKeyframe(propertyXName, keyframes[0]); - } - if (propertyYName != null) { - y = PropertyValuesHolder.ofKeyframe(propertyYName, keyframes[1]); - } - 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); - } - } } private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser) diff --git a/core/java/android/util/PathParser.java b/core/java/android/util/PathParser.java index f90ce51..f4a0448 100644 --- a/core/java/android/util/PathParser.java +++ b/core/java/android/util/PathParser.java @@ -45,6 +45,9 @@ public class PathParser { * @return an array of the PathDataNode. */ public static PathDataNode[] createNodesFromPathData(String pathData) { + if (pathData == null) { + return null; + } int start = 0; int end = 1; @@ -64,6 +67,57 @@ public class PathParser { return list.toArray(new PathDataNode[list.size()]); } + /** + * @param source The array of PathDataNode to be duplicated. + * @return a deep copy of the <code>source</code>. + */ + public static PathDataNode[] deepCopyNodes(PathDataNode[] source) { + PathDataNode[] copy = new PathParser.PathDataNode[source.length]; + for (int i = 0; i < source.length; i ++) { + copy[i] = new PathDataNode(source[i]); + } + return copy; + } + + /** + * @param nodesFrom The source path represented in an array of PathDataNode + * @param nodesTo The target path represented in an array of PathDataNode + * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code> + */ + public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) { + if (nodesFrom == null || nodesTo == null) { + return false; + } + + if (nodesFrom.length != nodesTo.length) { + return false; + } + + for (int i = 0; i < nodesFrom.length; i ++) { + if (nodesFrom[i].mType != nodesTo[i].mType + || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) { + return false; + } + } + return true; + } + + /** + * Update the target's data to match the source. + * Before calling this, make sure canMorph(target, source) is true. + * + * @param target The target path represented in an array of PathDataNode + * @param source The source path represented in an array of PathDataNode + */ + public static void updateNodes(PathDataNode[] target, PathDataNode[] source) { + for (int i = 0; i < source.length; i ++) { + target[i].mType = source[i].mType; + for (int j = 0; j < source[i].mParams.length; j ++) { + target[i].mParams[j] = source[i].mParams[j]; + } + } + } + private static int nextStart(String s, int end) { char c; @@ -132,6 +186,11 @@ public class PathParser { return (comma > space) ? space : comma; } + /** + * Each PathDataNode represents one command in the "d" attribute of the svg + * file. + * An array of PathDataNode can represent the whole "d" attribute. + */ public static class PathDataNode { private char mType; private float[] mParams; @@ -146,6 +205,12 @@ public class PathParser { mParams = Arrays.copyOf(n.mParams, n.mParams.length); } + /** + * Convert an array of PathDataNode to Path. + * + * @param node The source array of PathDataNode. + * @param path The target Path object. + */ public static void nodesToPath(PathDataNode[] node, Path path) { float[] current = new float[4]; char previousCommand = 'm'; @@ -155,6 +220,23 @@ public class PathParser { } } + /** + * The current PathDataNode will be interpolated between the + * <code>nodeFrom</code> and <code>nodeTo</code> according to the + * <code>fraction</code>. + * + * @param nodeFrom The start value as a PathDataNode. + * @param nodeTo The end value as a PathDataNode + * @param fraction The fraction to interpolate. + */ + public void interpolatePathDataNode(PathDataNode nodeFrom, + PathDataNode nodeTo, float fraction) { + for (int i = 0; i < nodeFrom.mParams.length; i++) { + mParams[i] = nodeFrom.mParams[i] * (1 - fraction) + + nodeTo.mParams[i] * fraction; + } + } + private static void addCommand(Path path, float[] current, char previousCmd, char cmd, float[] val) { @@ -523,6 +605,5 @@ public class PathParser { ep1y = ep2y; } } - } } diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index f74a356..198333f 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -5400,6 +5400,9 @@ <enum name="floatType" value="0" /> <!-- valueFrom and valueTo are integers. --> <enum name="intType" value="1" /> + <!-- valueFrom and valueTo are paths defined as strings. + This type is used for path morphing in AnimatedVectorDrawable. --> + <enum name="pathType" value="2" /> </attr> </declare-styleable> |
