diff options
8 files changed, 365 insertions, 82 deletions
diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index 39fcf733ecf5..0f5e95428173 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 f90ce51be891..f4a044861191 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 f74a356f0523..198333f9d6b3 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> diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java index b4d1fdc29828..7c6c67d51f54 100644 --- a/graphics/java/android/graphics/drawable/VectorDrawable.java +++ b/graphics/java/android/graphics/drawable/VectorDrawable.java @@ -1019,67 +1019,98 @@ public class VectorDrawable extends Drawable { } } - /* Setters and Getters */ + /* Setters and Getters, mostly used by animator from AnimatedVectorDrawable. */ + @SuppressWarnings("unused") + public PathParser.PathDataNode[] getPathData() { + return mNode; + } + + @SuppressWarnings("unused") + public void setPathData(PathParser.PathDataNode[] node) { + if (!PathParser.canMorph(mNode, node)) { + // This should not happen in the middle of animation. + mNode = PathParser.deepCopyNodes(node); + } else { + PathParser.updateNodes(mNode, node); + } + } + + @SuppressWarnings("unused") int getStroke() { return mStrokeColor; } + @SuppressWarnings("unused") void setStroke(int strokeColor) { mStrokeColor = strokeColor; } + @SuppressWarnings("unused") float getStrokeWidth() { return mStrokeWidth; } + @SuppressWarnings("unused") void setStrokeWidth(float strokeWidth) { mStrokeWidth = strokeWidth; } + @SuppressWarnings("unused") float getStrokeOpacity() { return mStrokeOpacity; } + @SuppressWarnings("unused") void setStrokeOpacity(float strokeOpacity) { mStrokeOpacity = strokeOpacity; } + @SuppressWarnings("unused") int getFill() { return mFillColor; } + @SuppressWarnings("unused") void setFill(int fillColor) { mFillColor = fillColor; } + @SuppressWarnings("unused") float getFillOpacity() { return mFillOpacity; } + @SuppressWarnings("unused") void setFillOpacity(float fillOpacity) { mFillOpacity = fillOpacity; } + @SuppressWarnings("unused") float getTrimPathStart() { return mTrimPathStart; } + @SuppressWarnings("unused") void setTrimPathStart(float trimPathStart) { mTrimPathStart = trimPathStart; } + @SuppressWarnings("unused") float getTrimPathEnd() { return mTrimPathEnd; } + @SuppressWarnings("unused") void setTrimPathEnd(float trimPathEnd) { mTrimPathEnd = trimPathEnd; } + @SuppressWarnings("unused") float getTrimPathOffset() { return mTrimPathOffset; } + @SuppressWarnings("unused") void setTrimPathOffset(float trimPathOffset) { mTrimPathOffset = trimPathOffset; } diff --git a/tests/VectorDrawableTest/res/anim/trim_path_animation05.xml b/tests/VectorDrawableTest/res/anim/trim_path_animation05.xml new file mode 100644 index 000000000000..7012f4b721b0 --- /dev/null +++ b/tests/VectorDrawableTest/res/anim/trim_path_animation05.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:ordering="sequentially" > + + <objectAnimator + android:duration="3000" + android:propertyName="pathData" + android:valueFrom="@string/triangle" + android:valueTo="@string/rectangle" + android:valueType="pathType"/> + + <objectAnimator + android:duration="3000" + android:propertyName="pathData" + android:valueFrom="@string/rectangle2" + android:valueTo="@string/equal2" + android:valueType="pathType"/> + +</set>
\ No newline at end of file diff --git a/tests/VectorDrawableTest/res/drawable/animation_vector_drawable01.xml b/tests/VectorDrawableTest/res/drawable/animation_vector_drawable01.xml index 0900b7c24af7..b37b19fb254e 100644 --- a/tests/VectorDrawableTest/res/drawable/animation_vector_drawable01.xml +++ b/tests/VectorDrawableTest/res/drawable/animation_vector_drawable01.xml @@ -19,9 +19,13 @@ <target android:name="pie1" android:animation="@anim/trim_path_animation01" /> + <target android:name="v" android:animation="@anim/trim_path_animation02" /> + <target + android:name="v" + android:animation="@anim/trim_path_animation05" /> <target android:name="rotationGroup" @@ -36,4 +40,4 @@ android:name="rotationGroup" android:animation="@anim/trim_path_animation04" /> -</animated-vector>
\ No newline at end of file +</animated-vector> diff --git a/tests/VectorDrawableTest/res/drawable/vector_drawable12.xml b/tests/VectorDrawableTest/res/drawable/vector_drawable12.xml index e28ec4144d2d..a212defb551c 100644 --- a/tests/VectorDrawableTest/res/drawable/vector_drawable12.xml +++ b/tests/VectorDrawableTest/res/drawable/vector_drawable12.xml @@ -32,27 +32,21 @@ android:name="pie1" android:fill="#00000000" android:pathData="M300,70 a230,230 0 1,0 1,0 z" - android:stroke="#FF00FF00" + android:stroke="#FF777777" android:strokeWidth="70" android:trimPathEnd=".75" android:trimPathOffset="0" android:trimPathStart="0" /> <path android:name="v" - android:fill="#FF00FF00" - android:pathData="M300,70 l 0,-70 70,70 -70,70z" /> + android:fill="#000000" + android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" /> <group android:name="translateToCenterGroup" android:rotation="0.0" android:translateX="200.0" android:translateY="200.0" > - <path - android:name="twoLines" - android:pathData="@string/twoLinePathData" - android:stroke="#FFFF0000" - android:strokeWidth="20" /> - <group android:name="rotationGroup2" android:pivotX="0.0" @@ -61,7 +55,7 @@ <path android:name="twoLines1" android:pathData="@string/twoLinePathData" - android:stroke="#FF00FF00" + android:stroke="#FFFF0000" android:strokeWidth="20" /> <group diff --git a/tests/VectorDrawableTest/res/values/strings.xml b/tests/VectorDrawableTest/res/values/strings.xml index b49a1aa64a0b..6ae3d7fec91f 100644 --- a/tests/VectorDrawableTest/res/values/strings.xml +++ b/tests/VectorDrawableTest/res/values/strings.xml @@ -16,4 +16,11 @@ <resources> <string name="twoLinePathData" >"M 0,0 v 100 M 0,0 h 100"</string> + + <string name="triangle" > "M300,70 l 0,-70 70,70 0,0 -70,70z"</string> + <string name="rectangle" >"M300,70 l 0,-70 70,0 0,140 -70,0 z"</string> + + <string name="rectangle2" >"M300,70 l 0,-70 70,0 0,70z M300,70 l 70,0 0,70 -70,0z"</string> + <string name="equal2" > "M300,35 l 0,-35 70,0 0,35z M300,105 l 70,0 0,35 -70,0z"</string> + </resources> |