From cf4832f69c8786b098ce18c24319021f8cd6733a Mon Sep 17 00:00:00 2001 From: ztenghui Date: Tue, 17 Jun 2014 09:54:45 -0700 Subject: [PATCH] Add path support into xml files for PathInterpolator and ObjectAnimator. The test case is showing that AnimatedVectorDrawable is able to use path to define time interpolator and object movement now. Change-Id: If3c0418265d0fd762c8f5f0bb8c39cce3ad34ef3 --- api/current.txt | 2 + .../android/animation/AnimatorInflater.java | 164 ++++-- core/java/android/util/PathParser.java | 528 ++++++++++++++++++ .../view/animation/PathInterpolator.java | 56 +- core/res/res/values/attrs.xml | 14 + core/res/res/values/public.xml | 2 + .../graphics/drawable/VectorDrawable.java | 496 +--------------- .../res/anim/trim_path_animation03.xml | 4 +- .../res/anim/trim_path_animation04.xml | 8 +- .../drawable/animation_vector_drawable01.xml | 3 + .../interpolator/custom_path_interpolator.xml | 2 + 11 files changed, 707 insertions(+), 572 deletions(-) create mode 100644 core/java/android/util/PathParser.java create mode 100644 tests/VectorDrawableTest/res/interpolator/custom_path_interpolator.xml diff --git a/api/current.txt b/api/current.txt index 65589d486897e..e85e1d3559f38 100644 --- a/api/current.txt +++ b/api/current.txt @@ -937,6 +937,8 @@ package android { field public static final int progressTintMode = 16843878; // 0x1010466 field public static final int prompt = 16843131; // 0x101017b field public static final int propertyName = 16843489; // 0x10102e1 + field public static final int propertyXName = 16843894; // 0x1010476 + field public static final int propertyYName = 16843895; // 0x1010477 field public static final int protectionLevel = 16842761; // 0x1010009 field public static final int publicKey = 16843686; // 0x10103a6 field public static final int queryActionMsg = 16843227; // 0x10101db diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index 06f5aca1b644f..39fcf733ecf54 100644 --- a/core/java/android/animation/AnimatorInflater.java +++ b/core/java/android/animation/AnimatorInflater.java @@ -17,11 +17,13 @@ package android.animation; import android.content.Context; import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; -import android.content.res.Resources.NotFoundException; +import android.graphics.Path; import android.util.AttributeSet; +import android.util.PathParser; import android.util.StateSet; import android.util.TypedValue; import android.util.Xml; @@ -158,7 +160,7 @@ public class AnimatorInflater { int stateIndex = 0; for (int i = 0; i < attributeCount; i++) { int attrName = attributeSet.getAttributeNameResource(i); - if (attrName == com.android.internal.R.attr.animation) { + if (attrName == R.attr.animation) { animator = loadAnimator(context, attributeSet.getAttributeResourceValue(i, 0)); } else { @@ -186,36 +188,43 @@ public class AnimatorInflater { } } + /** + * @param anim Null if this is a ValueAnimator, otherwise this is an + * ObjectAnimator + * @param arrayAnimator Incoming typed array for Animator's attributes. + * @param arrayObjectAnimator Incoming typed array for Object Animator's + * attributes. + */ + private static void parseAnimatorFromTypeArray(ValueAnimator anim, + TypedArray arrayAnimator, TypedArray arrayObjectAnimator) { + long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300); - private static void parseAnimatorFromTypeArray(ValueAnimator anim, TypedArray a) { - long duration = a.getInt(com.android.internal.R.styleable.Animator_duration, 300); + long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0); - long startDelay = a.getInt(com.android.internal.R.styleable.Animator_startOffset, 0); - - int valueType = a.getInt(com.android.internal.R.styleable.Animator_valueType, + int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, VALUE_TYPE_FLOAT); if (anim == null) { anim = new ValueAnimator(); } - TypeEvaluator evaluator = null; - int valueFromIndex = com.android.internal.R.styleable.Animator_valueFrom; - int valueToIndex = com.android.internal.R.styleable.Animator_valueTo; + TypeEvaluator evaluator = null; + int valueFromIndex = R.styleable.Animator_valueFrom; + int valueToIndex = R.styleable.Animator_valueTo; boolean getFloats = (valueType == VALUE_TYPE_FLOAT); - TypedValue tvFrom = a.peekValue(valueFromIndex); + TypedValue tvFrom = arrayAnimator.peekValue(valueFromIndex); boolean hasFrom = (tvFrom != null); int fromType = hasFrom ? tvFrom.type : 0; - TypedValue tvTo = a.peekValue(valueToIndex); + TypedValue tvTo = arrayAnimator.peekValue(valueToIndex); 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))) { + (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(); @@ -226,15 +235,15 @@ public class AnimatorInflater { float valueTo; if (hasFrom) { if (fromType == TypedValue.TYPE_DIMENSION) { - valueFrom = a.getDimension(valueFromIndex, 0f); + valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f); } else { - valueFrom = a.getFloat(valueFromIndex, 0f); + valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f); } if (hasTo) { if (toType == TypedValue.TYPE_DIMENSION) { - valueTo = a.getDimension(valueToIndex, 0f); + valueTo = arrayAnimator.getDimension(valueToIndex, 0f); } else { - valueTo = a.getFloat(valueToIndex, 0f); + valueTo = arrayAnimator.getFloat(valueToIndex, 0f); } anim.setFloatValues(valueFrom, valueTo); } else { @@ -242,9 +251,9 @@ public class AnimatorInflater { } } else { if (toType == TypedValue.TYPE_DIMENSION) { - valueTo = a.getDimension(valueToIndex, 0f); + valueTo = arrayAnimator.getDimension(valueToIndex, 0f); } else { - valueTo = a.getFloat(valueToIndex, 0f); + valueTo = arrayAnimator.getFloat(valueToIndex, 0f); } anim.setFloatValues(valueTo); } @@ -253,21 +262,21 @@ public class AnimatorInflater { int valueTo; if (hasFrom) { if (fromType == TypedValue.TYPE_DIMENSION) { - valueFrom = (int) a.getDimension(valueFromIndex, 0f); + valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f); } else if ((fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) { - valueFrom = a.getColor(valueFromIndex, 0); + valueFrom = arrayAnimator.getColor(valueFromIndex, 0); } else { - valueFrom = a.getInt(valueFromIndex, 0); + valueFrom = arrayAnimator.getInt(valueFromIndex, 0); } if (hasTo) { if (toType == TypedValue.TYPE_DIMENSION) { - valueTo = (int) a.getDimension(valueToIndex, 0f); + valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f); } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { - valueTo = a.getColor(valueToIndex, 0); + valueTo = arrayAnimator.getColor(valueToIndex, 0); } else { - valueTo = a.getInt(valueToIndex, 0); + valueTo = arrayAnimator.getInt(valueToIndex, 0); } anim.setIntValues(valueFrom, valueTo); } else { @@ -276,12 +285,12 @@ public class AnimatorInflater { } else { if (hasTo) { if (toType == TypedValue.TYPE_DIMENSION) { - valueTo = (int) a.getDimension(valueToIndex, 0f); + valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f); } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && - (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { - valueTo = a.getColor(valueToIndex, 0); + (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueTo = arrayAnimator.getColor(valueToIndex, 0); } else { - valueTo = a.getInt(valueToIndex, 0); + valueTo = arrayAnimator.getInt(valueToIndex, 0); } anim.setIntValues(valueTo); } @@ -291,18 +300,59 @@ public class AnimatorInflater { anim.setDuration(duration); anim.setStartDelay(startDelay); - if (a.hasValue(com.android.internal.R.styleable.Animator_repeatCount)) { + if (arrayAnimator.hasValue(R.styleable.Animator_repeatCount)) { anim.setRepeatCount( - a.getInt(com.android.internal.R.styleable.Animator_repeatCount, 0)); + arrayAnimator.getInt(R.styleable.Animator_repeatCount, 0)); } - if (a.hasValue(com.android.internal.R.styleable.Animator_repeatMode)) { + if (arrayAnimator.hasValue(R.styleable.Animator_repeatMode)) { anim.setRepeatMode( - a.getInt(com.android.internal.R.styleable.Animator_repeatMode, + 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) @@ -338,11 +388,11 @@ public class AnimatorInflater { anim = new AnimatorSet(); TypedArray a; if (theme != null) { - a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AnimatorSet, 0, 0); + a = theme.obtainStyledAttributes(attrs, R.styleable.AnimatorSet, 0, 0); } else { - a = res.obtainAttributes(attrs, com.android.internal.R.styleable.AnimatorSet); + a = res.obtainAttributes(attrs, R.styleable.AnimatorSet); } - int ordering = a.getInt(com.android.internal.R.styleable.AnimatorSet_ordering, + int ordering = a.getInt(R.styleable.AnimatorSet_ordering, TOGETHER); createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering); a.recycle(); @@ -380,19 +430,6 @@ public class AnimatorInflater { loadAnimator(res, theme, attrs, anim); - TypedArray a; - if (theme != null) { - a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyAnimator, 0, 0); - } else { - a = res.obtainAttributes(attrs, R.styleable.PropertyAnimator); - } - - String propertyName = a.getString(R.styleable.PropertyAnimator_propertyName); - - anim.setPropertyName(propertyName); - - a.recycle(); - return anim; } @@ -402,26 +439,41 @@ public class AnimatorInflater { * * @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) throws NotFoundException { - TypedArray a; + TypedArray arrayAnimator = null; + TypedArray arrayObjectAnimator = null; + if (theme != null) { - a = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0); + arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0); } else { - a = res.obtainAttributes(attrs, R.styleable.Animator); + arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator); } - parseAnimatorFromTypeArray(anim, a); + // 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); + } + } + parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator); final int resID = - a.getResourceId(com.android.internal.R.styleable.Animator_interpolator, 0); + arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0); if (resID > 0) { anim.setInterpolator(AnimationUtils.loadInterpolator(res, theme, resID)); } - a.recycle(); + + arrayAnimator.recycle(); + arrayObjectAnimator.recycle(); return anim; } diff --git a/core/java/android/util/PathParser.java b/core/java/android/util/PathParser.java new file mode 100644 index 0000000000000..f90ce51be891f --- /dev/null +++ b/core/java/android/util/PathParser.java @@ -0,0 +1,528 @@ +/* + * 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. + */ + +package android.util; + +import android.graphics.Path; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * @hide + */ +public class PathParser { + static final String LOGTAG = PathParser.class.getSimpleName(); + + /** + * @param pathData The string representing a path, the same as "d" string in svg file. + * @return the generated Path object. + */ + public static Path createPathFromPathData(String pathData) { + Path path = new Path(); + PathDataNode[] nodes = createNodesFromPathData(pathData); + if (nodes != null) { + PathDataNode.nodesToPath(nodes, path); + return path; + } + return null; + } + + /** + * @param pathData The string representing a path, the same as "d" string in svg file. + * @return an array of the PathDataNode. + */ + public static PathDataNode[] createNodesFromPathData(String pathData) { + int start = 0; + int end = 1; + + ArrayList list = new ArrayList(); + while (end < pathData.length()) { + end = nextStart(pathData, end); + String s = pathData.substring(start, end); + float[] val = getFloats(s); + addNode(list, s.charAt(0), val); + + start = end; + end++; + } + if ((end - start) == 1 && start < pathData.length()) { + addNode(list, pathData.charAt(start), new float[0]); + } + return list.toArray(new PathDataNode[list.size()]); + } + + private static int nextStart(String s, int end) { + char c; + + while (end < s.length()) { + c = s.charAt(end); + if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { + return end; + } + end++; + } + return end; + } + + private static void addNode(ArrayList list, char cmd, float[] val) { + list.add(new PathDataNode(cmd, val)); + } + + + /** + * Parse the floats in the string. + * This is an optimized version of parseFloat(s.split(",|\\s")); + * + * @param s the string containing a command and list of floats + * @return array of floats + */ + private static float[] getFloats(String s) { + if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { + return new float[0]; + } + try { + float[] tmp = new float[s.length()]; + int count = 0; + int pos = 1, end; + while ((end = extract(s, pos)) >= 0) { + if (pos < end) { + tmp[count++] = Float.parseFloat(s.substring(pos, end)); + } + pos = end + 1; + } + // handle the final float if there is one + if (pos < s.length()) { + tmp[count++] = Float.parseFloat(s.substring(pos, s.length())); + } + return Arrays.copyOf(tmp, count); + } catch (NumberFormatException e){ + Log.e(LOGTAG,"error in parsing \""+s+"\""); + throw e; + } + } + + /** + * Calculate the position of the next comma or space + * @param s the string to search + * @param start the position to start searching + * @return the position of the next comma or space or -1 if none found + */ + private static int extract(String s, int start) { + int space = s.indexOf(' ', start); + int comma = s.indexOf(',', start); + if (space == -1) { + return comma; + } + if (comma == -1) { + return space; + } + return (comma > space) ? space : comma; + } + + public static class PathDataNode { + private char mType; + private float[] mParams; + + private PathDataNode(char type, float[] params) { + mType = type; + mParams = params; + } + + private PathDataNode(PathDataNode n) { + mType = n.mType; + mParams = Arrays.copyOf(n.mParams, n.mParams.length); + } + + public static void nodesToPath(PathDataNode[] node, Path path) { + float[] current = new float[4]; + char previousCommand = 'm'; + for (int i = 0; i < node.length; i++) { + addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); + previousCommand = node[i].mType; + } + } + + private static void addCommand(Path path, float[] current, + char previousCmd, char cmd, float[] val) { + + int incr = 2; + float currentX = current[0]; + float currentY = current[1]; + float ctrlPointX = current[2]; + float ctrlPointY = current[3]; + float reflectiveCtrlPointX; + float reflectiveCtrlPointY; + + switch (cmd) { + case 'z': + case 'Z': + path.close(); + return; + case 'm': + case 'M': + case 'l': + case 'L': + case 't': + case 'T': + incr = 2; + break; + case 'h': + case 'H': + case 'v': + case 'V': + incr = 1; + break; + case 'c': + case 'C': + incr = 6; + break; + case 's': + case 'S': + case 'q': + case 'Q': + incr = 4; + break; + case 'a': + case 'A': + incr = 7; + break; + } + for (int k = 0; k < val.length; k += incr) { + switch (cmd) { + case 'm': // moveto - Start a new sub-path (relative) + path.rMoveTo(val[k + 0], val[k + 1]); + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'M': // moveto - Start a new sub-path + path.moveTo(val[k + 0], val[k + 1]); + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'l': // lineto - Draw a line from the current point (relative) + path.rLineTo(val[k + 0], val[k + 1]); + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'L': // lineto - Draw a line from the current point + path.lineTo(val[k + 0], val[k + 1]); + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'z': // closepath - Close the current subpath + case 'Z': // closepath - Close the current subpath + path.close(); + break; + case 'h': // horizontal lineto - Draws a horizontal line (relative) + path.rLineTo(val[k + 0], 0); + currentX += val[k + 0]; + break; + case 'H': // horizontal lineto - Draws a horizontal line + path.lineTo(val[k + 0], currentY); + currentX = val[k + 0]; + break; + case 'v': // vertical lineto - Draws a vertical line from the current point (r) + path.rLineTo(0, val[k + 0]); + currentY += val[k + 0]; + break; + case 'V': // vertical lineto - Draws a vertical line from the current point + path.lineTo(currentX, val[k + 0]); + currentY = val[k + 0]; + break; + case 'c': // curveto - Draws a cubic Bézier curve (relative) + path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], + val[k + 4], val[k + 5]); + + ctrlPointX = currentX + val[k + 2]; + ctrlPointY = currentY + val[k + 3]; + currentX += val[k + 4]; + currentY += val[k + 5]; + + break; + case 'C': // curveto - Draws a cubic Bézier curve + path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], + val[k + 4], val[k + 5]); + currentX = val[k + 4]; + currentY = val[k + 5]; + ctrlPointX = val[k + 2]; + ctrlPointY = val[k + 3]; + break; + case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) + reflectiveCtrlPointX = 0; + reflectiveCtrlPointY = 0; + if (previousCmd == 'c' || previousCmd == 's' + || previousCmd == 'C' || previousCmd == 'S') { + reflectiveCtrlPointX = currentX - ctrlPointX; + reflectiveCtrlPointY = currentY - ctrlPointY; + } + path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1], + val[k + 2], val[k + 3]); + + ctrlPointX = currentX + val[k + 0]; + ctrlPointY = currentY + val[k + 1]; + currentX += val[k + 2]; + currentY += val[k + 3]; + break; + case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) + reflectiveCtrlPointX = currentX; + reflectiveCtrlPointY = currentY; + if (previousCmd == 'c' || previousCmd == 's' + || previousCmd == 'C' || previousCmd == 'S') { + reflectiveCtrlPointX = 2 * currentX - ctrlPointX; + reflectiveCtrlPointY = 2 * currentY - ctrlPointY; + } + path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = val[k + 0]; + ctrlPointY = val[k + 1]; + currentX = val[k + 2]; + currentY = val[k + 3]; + break; + case 'q': // Draws a quadratic Bézier (relative) + path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = currentX + val[k + 0]; + ctrlPointY = currentY + val[k + 1]; + currentX += val[k + 2]; + currentY += val[k + 3]; + break; + case 'Q': // Draws a quadratic Bézier + path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = val[k + 0]; + ctrlPointY = val[k + 1]; + currentX = val[k + 2]; + currentY = val[k + 3]; + break; + case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) + reflectiveCtrlPointX = 0; + reflectiveCtrlPointY = 0; + if (previousCmd == 'q' || previousCmd == 't' + || previousCmd == 'Q' || previousCmd == 'T') { + reflectiveCtrlPointX = currentX - ctrlPointX; + reflectiveCtrlPointY = currentY - ctrlPointY; + } + path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1]); + ctrlPointX = currentX + reflectiveCtrlPointX; + ctrlPointY = currentY + reflectiveCtrlPointY; + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'T': // Draws a quadratic Bézier curve (reflective control point) + reflectiveCtrlPointX = currentX; + reflectiveCtrlPointY = currentY; + if (previousCmd == 'q' || previousCmd == 't' + || previousCmd == 'Q' || previousCmd == 'T') { + reflectiveCtrlPointX = 2 * currentX - ctrlPointX; + reflectiveCtrlPointY = 2 * currentY - ctrlPointY; + } + path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1]); + ctrlPointX = reflectiveCtrlPointX; + ctrlPointY = reflectiveCtrlPointY; + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'a': // Draws an elliptical arc + // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) + drawArc(path, + currentX, + currentY, + val[k + 5] + currentX, + val[k + 6] + currentY, + val[k + 0], + val[k + 1], + val[k + 2], + val[k + 3] != 0, + val[k + 4] != 0); + currentX += val[k + 5]; + currentY += val[k + 6]; + ctrlPointX = currentX; + ctrlPointY = currentY; + break; + case 'A': // Draws an elliptical arc + drawArc(path, + currentX, + currentY, + val[k + 5], + val[k + 6], + val[k + 0], + val[k + 1], + val[k + 2], + val[k + 3] != 0, + val[k + 4] != 0); + currentX = val[k + 5]; + currentY = val[k + 6]; + ctrlPointX = currentX; + ctrlPointY = currentY; + break; + } + previousCmd = cmd; + } + current[0] = currentX; + current[1] = currentY; + current[2] = ctrlPointX; + current[3] = ctrlPointY; + } + + private static void drawArc(Path p, + float x0, + float y0, + float x1, + float y1, + float a, + float b, + float theta, + boolean isMoreThanHalf, + boolean isPositiveArc) { + + /* Convert rotation angle from degrees to radians */ + double thetaD = Math.toRadians(theta); + /* Pre-compute rotation matrix entries */ + double cosTheta = Math.cos(thetaD); + double sinTheta = Math.sin(thetaD); + /* Transform (x0, y0) and (x1, y1) into unit space */ + /* using (inverse) rotation, followed by (inverse) scale */ + double x0p = (x0 * cosTheta + y0 * sinTheta) / a; + double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; + double x1p = (x1 * cosTheta + y1 * sinTheta) / a; + double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; + + /* Compute differences and averages */ + double dx = x0p - x1p; + double dy = y0p - y1p; + double xm = (x0p + x1p) / 2; + double ym = (y0p + y1p) / 2; + /* Solve for intersecting unit circles */ + double dsq = dx * dx + dy * dy; + if (dsq == 0.0) { + Log.w(LOGTAG, " Points are coincident"); + return; /* Points are coincident */ + } + double disc = 1.0 / dsq - 1.0 / 4.0; + if (disc < 0.0) { + Log.w(LOGTAG, "Points are too far apart " + dsq); + float adjust = (float) (Math.sqrt(dsq) / 1.99999); + drawArc(p, x0, y0, x1, y1, a * adjust, + b * adjust, theta, isMoreThanHalf, isPositiveArc); + return; /* Points are too far apart */ + } + double s = Math.sqrt(disc); + double sdx = s * dx; + double sdy = s * dy; + double cx; + double cy; + if (isMoreThanHalf == isPositiveArc) { + cx = xm - sdy; + cy = ym + sdx; + } else { + cx = xm + sdy; + cy = ym - sdx; + } + + double eta0 = Math.atan2((y0p - cy), (x0p - cx)); + + double eta1 = Math.atan2((y1p - cy), (x1p - cx)); + + double sweep = (eta1 - eta0); + if (isPositiveArc != (sweep >= 0)) { + if (sweep > 0) { + sweep -= 2 * Math.PI; + } else { + sweep += 2 * Math.PI; + } + } + + cx *= a; + cy *= b; + double tcx = cx; + cx = cx * cosTheta - cy * sinTheta; + cy = tcx * sinTheta + cy * cosTheta; + + arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); + } + + /** + * Converts an arc to cubic Bezier segments and records them in p. + * + * @param p The target for the cubic Bezier segments + * @param cx The x coordinate center of the ellipse + * @param cy The y coordinate center of the ellipse + * @param a The radius of the ellipse in the horizontal direction + * @param b The radius of the ellipse in the vertical direction + * @param e1x E(eta1) x coordinate of the starting point of the arc + * @param e1y E(eta2) y coordinate of the starting point of the arc + * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane + * @param start The start angle of the arc on the ellipse + * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse + */ + private static void arcToBezier(Path p, + double cx, + double cy, + double a, + double b, + double e1x, + double e1y, + double theta, + double start, + double sweep) { + // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html + // and http://www.spaceroots.org/documents/ellipse/node22.html + + // Maximum of 45 degrees per cubic Bezier segment + int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); + + double eta1 = start; + double cosTheta = Math.cos(theta); + double sinTheta = Math.sin(theta); + double cosEta1 = Math.cos(eta1); + double sinEta1 = Math.sin(eta1); + double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); + double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); + + double anglePerSegment = sweep / numSegments; + for (int i = 0; i < numSegments; i++) { + double eta2 = eta1 + anglePerSegment; + double sinEta2 = Math.sin(eta2); + double cosEta2 = Math.cos(eta2); + double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); + double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); + double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; + double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; + double tanDiff2 = Math.tan((eta2 - eta1) / 2); + double alpha = + Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; + double q1x = e1x + alpha * ep1x; + double q1y = e1y + alpha * ep1y; + double q2x = e2x - alpha * ep2x; + double q2y = e2y - alpha * ep2y; + + p.cubicTo((float) q1x, + (float) q1y, + (float) q2x, + (float) q2y, + (float) e2x, + (float) e2y); + eta1 = eta2; + e1x = e2x; + e1y = e2y; + ep1x = ep2x; + ep1y = ep2y; + } + } + + } +} diff --git a/core/java/android/view/animation/PathInterpolator.java b/core/java/android/view/animation/PathInterpolator.java index da12ffbc1bd62..945ecf00656a3 100644 --- a/core/java/android/view/animation/PathInterpolator.java +++ b/core/java/android/view/animation/PathInterpolator.java @@ -21,6 +21,7 @@ import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Path; import android.util.AttributeSet; +import android.util.PathParser; import android.view.InflateException; import com.android.internal.R; @@ -102,28 +103,40 @@ public class PathInterpolator implements Interpolator { } private void parseInterpolatorFromTypeArray(TypedArray a) { - if (!a.hasValue(R.styleable.PathInterpolator_controlX1)) { - throw new InflateException("pathInterpolator requires the controlX1 attribute"); - } else if (!a.hasValue(R.styleable.PathInterpolator_controlY1)) { - throw new InflateException("pathInterpolator requires the controlY1 attribute"); - } - float x1 = a.getFloat(R.styleable.PathInterpolator_controlX1, 0); - float y1 = a.getFloat(R.styleable.PathInterpolator_controlY1, 0); - - boolean hasX2 = a.hasValue(R.styleable.PathInterpolator_controlX2); - boolean hasY2 = a.hasValue(R.styleable.PathInterpolator_controlY2); - - if (hasX2 != hasY2) { - throw new InflateException( - "pathInterpolator requires both controlX2 and controlY2 for cubic Beziers."); - } - - if (!hasX2) { - initQuad(x1, y1); + // If there is pathData defined in the xml file, then the controls points + // will be all coming from pathData. + if (a.hasValue(R.styleable.PathInterpolator_pathData)) { + String pathData = a.getString(R.styleable.PathInterpolator_pathData); + Path path = PathParser.createPathFromPathData(pathData); + if (path == null) { + throw new InflateException("The path is null, which is created" + + " from " + pathData); + } + initPath(path); } else { - float x2 = a.getFloat(R.styleable.PathInterpolator_controlX2, 0); - float y2 = a.getFloat(R.styleable.PathInterpolator_controlY2, 0); - initCubic(x1, y1, x2, y2); + if (!a.hasValue(R.styleable.PathInterpolator_controlX1)) { + throw new InflateException("pathInterpolator requires the controlX1 attribute"); + } else if (!a.hasValue(R.styleable.PathInterpolator_controlY1)) { + throw new InflateException("pathInterpolator requires the controlY1 attribute"); + } + float x1 = a.getFloat(R.styleable.PathInterpolator_controlX1, 0); + float y1 = a.getFloat(R.styleable.PathInterpolator_controlY1, 0); + + boolean hasX2 = a.hasValue(R.styleable.PathInterpolator_controlX2); + boolean hasY2 = a.hasValue(R.styleable.PathInterpolator_controlY2); + + if (hasX2 != hasY2) { + throw new InflateException( + "pathInterpolator requires both controlX2 and controlY2 for cubic Beziers."); + } + + if (!hasX2) { + initQuad(x1, y1); + } else { + float x2 = a.getFloat(R.styleable.PathInterpolator_controlX2, 0); + float y2 = a.getFloat(R.styleable.PathInterpolator_controlY2, 0); + initCubic(x1, y1, x2, y2); + } } } @@ -216,5 +229,4 @@ public class PathInterpolator implements Interpolator { float endY = mY[endIndex]; return startY + (fraction * (endY - startY)); } - } diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 8d8f69e87df85..f74a356f0523c 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -5250,10 +5250,18 @@ + + + + + + @@ -5403,6 +5411,12 @@ + + + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index f6ad78bdc91a7..f73c9586788f6 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2207,6 +2207,8 @@ + + diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java index a1e1f7612a609..b4d1fdc298284 100644 --- a/graphics/java/android/graphics/drawable/VectorDrawable.java +++ b/graphics/java/android/graphics/drawable/VectorDrawable.java @@ -33,6 +33,7 @@ import android.graphics.PorterDuff.Mode; import android.util.ArrayMap; import android.util.AttributeSet; import android.util.Log; +import android.util.PathParser; import android.util.Xml; import com.android.internal.R; @@ -956,7 +957,7 @@ public class VectorDrawable extends Drawable { } - static class VPath { + private static class VPath { private int[] mThemeAttrs; int mStrokeColor = 0; @@ -974,7 +975,7 @@ public class VectorDrawable extends Drawable { Paint.Join mStrokeLineJoin = Paint.Join.MITER; float mStrokeMiterlimit = 4; - private VNode[] mNode = null; + private PathParser.PathDataNode[] mNode = null; private String mPathName; public VPath() { @@ -984,7 +985,7 @@ public class VectorDrawable extends Drawable { public void toPath(Path path) { path.reset(); if (mNode != null) { - VNode.createPath(mNode, path); + PathParser.PathDataNode.nodesToPath(mNode, path); } } @@ -1099,7 +1100,8 @@ public class VectorDrawable extends Drawable { } if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pathData] == 0) { - mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData)); + mNode = PathParser.createNodesFromPathData(a.getString( + R.styleable.VectorDrawablePath_pathData)); } if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fill] == 0) { @@ -1182,7 +1184,8 @@ public class VectorDrawable extends Drawable { } if (a.hasValue(R.styleable.VectorDrawablePath_pathData)) { - mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData)); + mNode = PathParser.createNodesFromPathData(a.getString( + R.styleable.VectorDrawablePath_pathData)); } mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor); @@ -1218,488 +1221,5 @@ public class VectorDrawable extends Drawable { mStrokeColor = applyAlpha(mStrokeColor, mStrokeOpacity); } } - - private static int nextStart(String s, int end) { - char c; - - while (end < s.length()) { - c = s.charAt(end); - if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { - return end; - } - end++; - } - return end; - } - - private void addNode(ArrayList list, char cmd, float[] val) { - list.add(new VectorDrawable.VNode(cmd, val)); - } - - /** - * parse the floats in the string - * this is an optimized version of - * parseFloat(s.split(",|\\s")); - * - * @param s the string containing a command and list of floats - * @return array of floats - */ - private static float[] getFloats(String s) { - if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { - return new float[0]; - } - try { - float[] tmp = new float[s.length()]; - int count = 0; - int pos = 1, end; - while ((end = extract(s, pos)) >= 0) { - if (pos < end) { - tmp[count++] = Float.parseFloat(s.substring(pos, end)); - } - pos = end + 1; - } - // handle the final float if there is one - if (pos < s.length()) { - tmp[count++] = Float.parseFloat(s.substring(pos, s.length())); - } - return Arrays.copyOf(tmp, count); - } catch (NumberFormatException e){ - Log.e(LOGTAG,"error in parsing \""+s+"\""); - throw e; - } - } - - /** - * calculate the position of the next comma or space - * @param s the string to search - * @param start the position to start searching - * @return the position of the next comma or space or -1 if none found - */ - private static int extract(String s, int start) { - int space = s.indexOf(' ', start); - int comma = s.indexOf(',', start); - if (space == -1) { - return comma; - } - if (comma == -1) { - return space; - } - return (comma > space) ? space : comma; - } - - private VectorDrawable.VNode[] parsePath(String value) { - int start = 0; - int end = 1; - - ArrayList list = new ArrayList(); - while (end < value.length()) { - end = nextStart(value, end); - String s = value.substring(start, end); - float[] val = getFloats(s); - addNode(list, s.charAt(0), val); - - start = end; - end++; - } - if ((end - start) == 1 && start < value.length()) { - - addNode(list, value.charAt(start), new float[0]); - } - return list.toArray(new VectorDrawable.VNode[list.size()]); - } - } - - private static class VNode { - private char mType; - private float[] mParams; - - public VNode(char type, float[] params) { - mType = type; - mParams = params; - } - - public VNode(VNode n) { - mType = n.mType; - mParams = Arrays.copyOf(n.mParams, n.mParams.length); - } - - public static void createPath(VNode[] node, Path path) { - float[] current = new float[4]; - char previousCommand = 'm'; - for (int i = 0; i < node.length; i++) { - addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); - previousCommand = node[i].mType; - } - } - - private static void addCommand(Path path, float[] current, - char previousCmd, char cmd, float[] val) { - - int incr = 2; - float currentX = current[0]; - float currentY = current[1]; - float ctrlPointX = current[2]; - float ctrlPointY = current[3]; - float reflectiveCtrlPointX; - float reflectiveCtrlPointY; - - switch (cmd) { - case 'z': - case 'Z': - path.close(); - return; - case 'm': - case 'M': - case 'l': - case 'L': - case 't': - case 'T': - incr = 2; - break; - case 'h': - case 'H': - case 'v': - case 'V': - incr = 1; - break; - case 'c': - case 'C': - incr = 6; - break; - case 's': - case 'S': - case 'q': - case 'Q': - incr = 4; - break; - case 'a': - case 'A': - incr = 7; - break; - } - for (int k = 0; k < val.length; k += incr) { - switch (cmd) { - case 'm': // moveto - Start a new sub-path (relative) - path.rMoveTo(val[k + 0], val[k + 1]); - currentX += val[k + 0]; - currentY += val[k + 1]; - break; - case 'M': // moveto - Start a new sub-path - path.moveTo(val[k + 0], val[k + 1]); - currentX = val[k + 0]; - currentY = val[k + 1]; - break; - case 'l': // lineto - Draw a line from the current point (relative) - path.rLineTo(val[k + 0], val[k + 1]); - currentX += val[k + 0]; - currentY += val[k + 1]; - break; - case 'L': // lineto - Draw a line from the current point - path.lineTo(val[k + 0], val[k + 1]); - currentX = val[k + 0]; - currentY = val[k + 1]; - break; - case 'z': // closepath - Close the current subpath - case 'Z': // closepath - Close the current subpath - path.close(); - break; - case 'h': // horizontal lineto - Draws a horizontal line (relative) - path.rLineTo(val[k + 0], 0); - currentX += val[k + 0]; - break; - case 'H': // horizontal lineto - Draws a horizontal line - path.lineTo(val[k + 0], currentY); - currentX = val[k + 0]; - break; - case 'v': // vertical lineto - Draws a vertical line from the current point (r) - path.rLineTo(0, val[k + 0]); - currentY += val[k + 0]; - break; - case 'V': // vertical lineto - Draws a vertical line from the current point - path.lineTo(currentX, val[k + 0]); - currentY = val[k + 0]; - break; - case 'c': // curveto - Draws a cubic Bézier curve (relative) - path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], - val[k + 4], val[k + 5]); - - ctrlPointX = currentX + val[k + 2]; - ctrlPointY = currentY + val[k + 3]; - currentX += val[k + 4]; - currentY += val[k + 5]; - - break; - case 'C': // curveto - Draws a cubic Bézier curve - path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], - val[k + 4], val[k + 5]); - currentX = val[k + 4]; - currentY = val[k + 5]; - ctrlPointX = val[k + 2]; - ctrlPointY = val[k + 3]; - break; - case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) - reflectiveCtrlPointX = 0; - reflectiveCtrlPointY = 0; - if (previousCmd == 'c' || previousCmd == 's' - || previousCmd == 'C' || previousCmd == 'S') { - reflectiveCtrlPointX = currentX - ctrlPointX; - reflectiveCtrlPointY = currentY - ctrlPointY; - } - path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, - val[k + 0], val[k + 1], - val[k + 2], val[k + 3]); - - ctrlPointX = currentX + val[k + 0]; - ctrlPointY = currentY + val[k + 1]; - currentX += val[k + 2]; - currentY += val[k + 3]; - break; - case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) - reflectiveCtrlPointX = currentX; - reflectiveCtrlPointY = currentY; - if (previousCmd == 'c' || previousCmd == 's' - || previousCmd == 'C' || previousCmd == 'S') { - reflectiveCtrlPointX = 2 * currentX - ctrlPointX; - reflectiveCtrlPointY = 2 * currentY - ctrlPointY; - } - path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, - val[k + 0], val[k + 1], val[k + 2], val[k + 3]); - ctrlPointX = val[k + 0]; - ctrlPointY = val[k + 1]; - currentX = val[k + 2]; - currentY = val[k + 3]; - break; - case 'q': // Draws a quadratic Bézier (relative) - path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); - ctrlPointX = currentX + val[k + 0]; - ctrlPointY = currentY + val[k + 1]; - currentX += val[k + 2]; - currentY += val[k + 3]; - break; - case 'Q': // Draws a quadratic Bézier - path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); - ctrlPointX = val[k + 0]; - ctrlPointY = val[k + 1]; - currentX = val[k + 2]; - currentY = val[k + 3]; - break; - case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) - reflectiveCtrlPointX = 0; - reflectiveCtrlPointY = 0; - if (previousCmd == 'q' || previousCmd == 't' - || previousCmd == 'Q' || previousCmd == 'T') { - reflectiveCtrlPointX = currentX - ctrlPointX; - reflectiveCtrlPointY = currentY - ctrlPointY; - } - path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, - val[k + 0], val[k + 1]); - ctrlPointX = currentX + reflectiveCtrlPointX; - ctrlPointY = currentY + reflectiveCtrlPointY; - currentX += val[k + 0]; - currentY += val[k + 1]; - break; - case 'T': // Draws a quadratic Bézier curve (reflective control point) - reflectiveCtrlPointX = currentX; - reflectiveCtrlPointY = currentY; - if (previousCmd == 'q' || previousCmd == 't' - || previousCmd == 'Q' || previousCmd == 'T') { - reflectiveCtrlPointX = 2 * currentX - ctrlPointX; - reflectiveCtrlPointY = 2 * currentY - ctrlPointY; - } - path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, - val[k + 0], val[k + 1]); - ctrlPointX = reflectiveCtrlPointX; - ctrlPointY = reflectiveCtrlPointY; - currentX = val[k + 0]; - currentY = val[k + 1]; - break; - case 'a': // Draws an elliptical arc - // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) - drawArc(path, - currentX, - currentY, - val[k + 5] + currentX, - val[k + 6] + currentY, - val[k + 0], - val[k + 1], - val[k + 2], - val[k + 3] != 0, - val[k + 4] != 0); - currentX += val[k + 5]; - currentY += val[k + 6]; - ctrlPointX = currentX; - ctrlPointY = currentY; - break; - case 'A': // Draws an elliptical arc - drawArc(path, - currentX, - currentY, - val[k + 5], - val[k + 6], - val[k + 0], - val[k + 1], - val[k + 2], - val[k + 3] != 0, - val[k + 4] != 0); - currentX = val[k + 5]; - currentY = val[k + 6]; - ctrlPointX = currentX; - ctrlPointY = currentY; - break; - } - previousCmd = cmd; - } - current[0] = currentX; - current[1] = currentY; - current[2] = ctrlPointX; - current[3] = ctrlPointY; - } - - private static void drawArc(Path p, - float x0, - float y0, - float x1, - float y1, - float a, - float b, - float theta, - boolean isMoreThanHalf, - boolean isPositiveArc) { - - /* Convert rotation angle from degrees to radians */ - double thetaD = Math.toRadians(theta); - /* Pre-compute rotation matrix entries */ - double cosTheta = Math.cos(thetaD); - double sinTheta = Math.sin(thetaD); - /* Transform (x0, y0) and (x1, y1) into unit space */ - /* using (inverse) rotation, followed by (inverse) scale */ - double x0p = (x0 * cosTheta + y0 * sinTheta) / a; - double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; - double x1p = (x1 * cosTheta + y1 * sinTheta) / a; - double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; - - /* Compute differences and averages */ - double dx = x0p - x1p; - double dy = y0p - y1p; - double xm = (x0p + x1p) / 2; - double ym = (y0p + y1p) / 2; - /* Solve for intersecting unit circles */ - double dsq = dx * dx + dy * dy; - if (dsq == 0.0) { - Log.w(LOGTAG, " Points are coincident"); - return; /* Points are coincident */ - } - double disc = 1.0 / dsq - 1.0 / 4.0; - if (disc < 0.0) { - Log.w(LOGTAG, "Points are too far apart " + dsq); - float adjust = (float) (Math.sqrt(dsq) / 1.99999); - drawArc(p, x0, y0, x1, y1, a * adjust, - b * adjust, theta, isMoreThanHalf, isPositiveArc); - return; /* Points are too far apart */ - } - double s = Math.sqrt(disc); - double sdx = s * dx; - double sdy = s * dy; - double cx; - double cy; - if (isMoreThanHalf == isPositiveArc) { - cx = xm - sdy; - cy = ym + sdx; - } else { - cx = xm + sdy; - cy = ym - sdx; - } - - double eta0 = Math.atan2((y0p - cy), (x0p - cx)); - - double eta1 = Math.atan2((y1p - cy), (x1p - cx)); - - double sweep = (eta1 - eta0); - if (isPositiveArc != (sweep >= 0)) { - if (sweep > 0) { - sweep -= 2 * Math.PI; - } else { - sweep += 2 * Math.PI; - } - } - - cx *= a; - cy *= b; - double tcx = cx; - cx = cx * cosTheta - cy * sinTheta; - cy = tcx * sinTheta + cy * cosTheta; - - arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); - } - - /** - * Converts an arc to cubic Bezier segments and records them in p. - * - * @param p The target for the cubic Bezier segments - * @param cx The x coordinate center of the ellipse - * @param cy The y coordinate center of the ellipse - * @param a The radius of the ellipse in the horizontal direction - * @param b The radius of the ellipse in the vertical direction - * @param e1x E(eta1) x coordinate of the starting point of the arc - * @param e1y E(eta2) y coordinate of the starting point of the arc - * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane - * @param start The start angle of the arc on the ellipse - * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse - */ - private static void arcToBezier(Path p, - double cx, - double cy, - double a, - double b, - double e1x, - double e1y, - double theta, - double start, - double sweep) { - // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html - // and http://www.spaceroots.org/documents/ellipse/node22.html - - // Maximum of 45 degrees per cubic Bezier segment - int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); - - double eta1 = start; - double cosTheta = Math.cos(theta); - double sinTheta = Math.sin(theta); - double cosEta1 = Math.cos(eta1); - double sinEta1 = Math.sin(eta1); - double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); - double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); - - double anglePerSegment = sweep / numSegments; - for (int i = 0; i < numSegments; i++) { - double eta2 = eta1 + anglePerSegment; - double sinEta2 = Math.sin(eta2); - double cosEta2 = Math.cos(eta2); - double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); - double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); - double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; - double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; - double tanDiff2 = Math.tan((eta2 - eta1) / 2); - double alpha = - Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; - double q1x = e1x + alpha * ep1x; - double q1y = e1y + alpha * ep1y; - double q2x = e2x - alpha * ep2x; - double q2y = e2y - alpha * ep2y; - - p.cubicTo((float) q1x, - (float) q1y, - (float) q2x, - (float) q2y, - (float) e2x, - (float) e2y); - eta1 = eta2; - e1x = e2x; - e1y = e2y; - ep1x = ep2x; - ep1y = ep2y; - } - } - } } diff --git a/tests/VectorDrawableTest/res/anim/trim_path_animation03.xml b/tests/VectorDrawableTest/res/anim/trim_path_animation03.xml index 72beba2f7b15b..0c1073e5b2cd4 100644 --- a/tests/VectorDrawableTest/res/anim/trim_path_animation03.xml +++ b/tests/VectorDrawableTest/res/anim/trim_path_animation03.xml @@ -21,6 +21,8 @@ android:duration="6000" android:propertyName="rotation" android:valueFrom="0" - android:valueTo="360"/> + android:valueTo="360" + android:interpolator="@interpolator/custom_path_interpolator" + /> \ No newline at end of file diff --git a/tests/VectorDrawableTest/res/anim/trim_path_animation04.xml b/tests/VectorDrawableTest/res/anim/trim_path_animation04.xml index ff866682f3486..4d0aae1c93149 100644 --- a/tests/VectorDrawableTest/res/anim/trim_path_animation04.xml +++ b/tests/VectorDrawableTest/res/anim/trim_path_animation04.xml @@ -16,11 +16,9 @@ --> - - + android:propertyXName="translateX" + android:propertyYName="translateY" + android:pathData="m0,0 q 150, 300 150, 0 t 150, 0, t 150, 0 t -150 0 t -150 0 t -150 0 z" /> \ 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 b8681b6556ef8..0900b7c24af75 100644 --- a/tests/VectorDrawableTest/res/drawable/animation_vector_drawable01.xml +++ b/tests/VectorDrawableTest/res/drawable/animation_vector_drawable01.xml @@ -32,5 +32,8 @@ + \ No newline at end of file diff --git a/tests/VectorDrawableTest/res/interpolator/custom_path_interpolator.xml b/tests/VectorDrawableTest/res/interpolator/custom_path_interpolator.xml new file mode 100644 index 0000000000000..0cffa0a9009bc --- /dev/null +++ b/tests/VectorDrawableTest/res/interpolator/custom_path_interpolator.xml @@ -0,0 +1,2 @@ + \ No newline at end of file