Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eb034fbc authored by ztenghui's avatar ztenghui
Browse files

AVD now support path morphing.

Basically extended the ValueAnimator to support a new type: pathType.
Add the PathDataEvaluator internally to interpolate path data.
Update test to show the path morphing.

Change-Id: I89db0199cbc12e3041790a6115f3f50b80213cdb
parent 17e64ffd
Loading
Loading
Loading
Loading
+197 −69
Original line number Diff line number Diff line
@@ -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
     *
@@ -188,6 +193,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
@@ -209,18 +264,22 @@ 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;

        // 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) &&
@@ -229,7 +288,133 @@ public class AnimatorInflater {
                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)
+82 −1
Original line number Diff line number Diff line
@@ -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;
            }
        }

    }
}
+3 −0
Original line number Diff line number Diff line
@@ -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>

+32 −1
Original line number Diff line number Diff line
@@ -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;
        }
+35 −0
Original line number Diff line number Diff line
<?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
Loading