diff --git a/README.md b/README.md index d389a3a..0dd6564 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Animate any attribute in a specific path in the VectorDrawable `fillColor`, `strokeColor`, `strokeAlpha`, `fillAlpha`, `size`, `width`, `height`, `scale`, `scaleX`, `scaleY`, `rotation`, `translationX`, `translationY`, `trimPathStart`, `trimPathEnd`, `trimPathOffset`. -- **Path morphing:** +- **Path morphing on API +11 :** 💪 @@ -113,7 +113,7 @@ Add the following dependency to your module `build.gradle` file: ```gradle dependencies { ... - compile 'com.github.tarek360.RichPath:animator:0.0.4' + compile 'com.github.tarek360.RichPath:animator:0.0.5' } ``` @@ -147,10 +147,6 @@ RichPathAnimator .start(); ``` -## TODO -- Optimize the path morphing animation performance. -- ... - ## Credits diff --git a/animator/src/main/java/com/richpathanimator/AnimationBuilder.java b/animator/src/main/java/com/richpathanimator/AnimationBuilder.java index 992b26c..6feec15 100644 --- a/animator/src/main/java/com/richpathanimator/AnimationBuilder.java +++ b/animator/src/main/java/com/richpathanimator/AnimationBuilder.java @@ -3,9 +3,12 @@ import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.util.Log; import android.view.animation.Interpolator; import com.richpath.RichPath; +import com.richpath.pathparser.PathDataNode; +import com.richpath.pathparser.PathParserCompat; import java.util.ArrayList; import java.util.List; @@ -175,9 +178,20 @@ private AnimationBuilder color(String propertyName, int... colors) { } public AnimationBuilder pathData(String... pathData) { + + PathDataNode[][] pathDataNodes = new PathDataNode[pathData.length][]; + for (int i = 0; i < pathData.length; i++) { + pathDataNodes[i] = PathParserCompat.createNodesFromPathData(pathData[i]); + } + + if (!PathParserCompat.canMorph(pathDataNodes)) { + Log.w("RichPathAnimator", "the paths aren't compatible for morphing. The paths should have exact same length of commands, and exact same length of parameters for each command"); + return this; + } + for (final RichPath path : paths) { ObjectAnimator objectAnimator = - ObjectAnimator.ofObject(path, "pathData", new PathEvaluator(), pathData); + ObjectAnimator.ofObject(path, "pathDataNodes", new PathEvaluator(), pathDataNodes); applyAnimatorProperties(objectAnimator, path); } return this; diff --git a/animator/src/main/java/com/richpathanimator/PathEvaluator.java b/animator/src/main/java/com/richpathanimator/PathEvaluator.java index ba60f6d..4c63c24 100644 --- a/animator/src/main/java/com/richpathanimator/PathEvaluator.java +++ b/animator/src/main/java/com/richpathanimator/PathEvaluator.java @@ -2,61 +2,34 @@ import android.animation.TypeEvaluator; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import com.richpath.pathparser.PathDataNode; +import com.richpath.pathparser.PathParserCompat; /** * Created by tarek on 7/14/17. */ -public class PathEvaluator implements TypeEvaluator { +public class PathEvaluator implements TypeEvaluator { - private String pathHolder; - private String startPath; - private String endPath; - - private List startFloats = new ArrayList<>(); - private List endFloats = new ArrayList<>(); + private PathDataNode[] evaluatedNodes; @Override - public String evaluate(float fraction, String startPath, String endPath) { - - - if (pathHolder == null - || !this.startPath.equals(startPath) || !this.endPath.equals(endPath)) { - - this.pathHolder = endPath; - this.startPath = startPath; - this.endPath = endPath; - - startFloats.clear(); - endFloats.clear(); - - Pattern p = Pattern.compile("[-]?[0-9]*\\.?[0-9]+"); - - Matcher startMatcher = p.matcher(startPath); - Matcher endMatcher = p.matcher(endPath); + public PathDataNode[] evaluate(float fraction, PathDataNode[] startPathDataNodes, PathDataNode[] endPathDataNodes) { - while (startMatcher.find() && endMatcher.find()) { - startFloats.add(Float.parseFloat(startMatcher.group())); - endFloats.add(Float.parseFloat(endMatcher.group())); - pathHolder = p.matcher(pathHolder).replaceFirst("#"); - } + if (evaluatedNodes == null) { + evaluatedNodes = PathParserCompat.deepCopyNodes(startPathDataNodes); } - String evaluatedPath = pathHolder; - - for (int i = 0; i < startFloats.size(); i++) { - - float startFloat = startFloats.get(i); - float value = startFloat + fraction * (endFloats.get(i) - startFloat); - - evaluatedPath = evaluatedPath.replaceFirst("#", String.valueOf(value)); + for (int i = 0; i < startPathDataNodes.length; i++) { + for (int j = 0; j < startPathDataNodes[i].getParams().length; j++) { + float startFloat = startPathDataNodes[i].getParams()[j]; + float value = startFloat + fraction * (endPathDataNodes[i].getParams()[j] - startFloat); + evaluatedNodes[i].getParams()[j] = value; + } } - return evaluatedPath; + return evaluatedNodes; } + } diff --git a/app/src/main/res/drawable/square.xml b/app/src/main/res/drawable/square.xml deleted file mode 100644 index 433cc38..0000000 --- a/app/src/main/res/drawable/square.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/richpath/src/main/java/com/richpath/RichPath.java b/richpath/src/main/java/com/richpath/RichPath.java index a35c56d..f4f5956 100644 --- a/richpath/src/main/java/com/richpath/RichPath.java +++ b/richpath/src/main/java/com/richpath/RichPath.java @@ -11,7 +11,9 @@ import com.richpath.listener.OnRichPathUpdatedListener; import com.richpath.model.Group; +import com.richpath.pathparser.PathDataNode; import com.richpath.pathparser.PathParser; +import com.richpath.pathparser.PathParserCompat; import com.richpath.util.PathUtils; import com.richpath.util.XmlParser; @@ -60,7 +62,7 @@ public class RichPath extends Path { private Path originalPath; - private String pathData; + private PathDataNode[] pathDataNodes; private List matrices; public RichPath(String pathData) { @@ -377,14 +379,17 @@ void scaleStrokeWidth(float scale) { paint.setStrokeWidth(strokeWidth * scale); } + public void setPathData(String pathData) { + setPathDataNodes(PathParserCompat.createNodesFromPathData(pathData)); + } - public String getPathData() { - return pathData; + public PathDataNode[] getPathDataNodes() { + return pathDataNodes; } - public void setPathData(String pathData) { - PathUtils.setPathData(this, pathData); - this.pathData = pathData; + public void setPathDataNodes(PathDataNode[] pathDataNodes) { + PathUtils.setPathDataNodes(this, pathDataNodes); + this.pathDataNodes = pathDataNodes; for (Matrix matrix : matrices) { transform(matrix); @@ -395,7 +400,9 @@ public void setPathData(String pathData) { public void inflate(Context context, XmlResourceParser xpp) { - pathData = XmlParser.getAttributeString(context, xpp, "pathData", name); + String pathData = XmlParser.getAttributeString(context, xpp, "pathData", name); + + pathDataNodes = PathParserCompat.createNodesFromPathData(pathData); name = XmlParser.getAttributeString(context, xpp, "name", name); diff --git a/richpath/src/main/java/com/richpath/pathparser/PathDataNode.java b/richpath/src/main/java/com/richpath/pathparser/PathDataNode.java new file mode 100644 index 0000000..7dcbaaa --- /dev/null +++ b/richpath/src/main/java/com/richpath/pathparser/PathDataNode.java @@ -0,0 +1,468 @@ +package com.richpath.pathparser; + +/** + * Created by tarek on 7/16/17. + */ + + +import android.graphics.Path; +import android.util.Log; + +import static com.richpath.pathparser.PathParserCompat.copyOfRange; + +/** + * Each PathDataNode represents one command in the "d" attribute of the svg + * file. + * An array of PathDataNode can represent the whole "d" attribute. + */ +public class PathDataNode { + private static final String LOGTAG = "PathDataNode"; + /*package*/ + char type; + float[] params; + + PathDataNode(char type, float[] params) { + this.type = type; + this.params = params; + } + + PathDataNode(PathDataNode n) { + type = n.type; + params = copyOfRange(n.params, 0, n.params.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[6]; + char previousCommand = 'm'; + for (int i = 0; i < node.length; i++) { + addCommand(path, current, previousCommand, node[i].type, node[i].params); + previousCommand = node[i].type; + } + } + + /** + * The current PathDataNode will be interpolated between the + * nodeFrom and nodeTo according to the + * fraction. + * + * @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.params.length; i++) { + params[i] = nodeFrom.params[i] * (1 - fraction) + + nodeTo.params[i] * fraction; + } + } + + 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 currentSegmentStartX = current[4]; + float currentSegmentStartY = current[5]; + float reflectiveCtrlPointX; + float reflectiveCtrlPointY; + + switch (cmd) { + case 'z': + case 'Z': + path.close(); + // Path is closed here, but we need to move the pen to the + // closed position. So we cache the segment's starting position, + // and restore it here. + currentX = currentSegmentStartX; + currentY = currentSegmentStartY; + ctrlPointX = currentSegmentStartX; + ctrlPointY = currentSegmentStartY; + path.moveTo(currentX, currentY); + break; + 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) + currentX += val[k + 0]; + currentY += val[k + 1]; + if (k > 0) { + // According to the spec, if a moveto is followed by multiple + // pairs of coordinates, the subsequent pairs are treated as + // implicit lineto commands. + path.rLineTo(val[k + 0], val[k + 1]); + } else { + path.rMoveTo(val[k + 0], val[k + 1]); + currentSegmentStartX = currentX; + currentSegmentStartY = currentY; + } + break; + case 'M': // moveto - Start a new sub-path + currentX = val[k + 0]; + currentY = val[k + 1]; + if (k > 0) { + // According to the spec, if a moveto is followed by multiple + // pairs of coordinates, the subsequent pairs are treated as + // implicit lineto commands. + path.lineTo(val[k + 0], val[k + 1]); + } else { + path.moveTo(val[k + 0], val[k + 1]); + currentSegmentStartX = currentX; + currentSegmentStartY = currentY; + } + 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 '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; + current[4] = currentSegmentStartX; + current[5] = currentSegmentStartY; + } + + public float[] getParams() { + return params; + } + + 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 = (int) Math.ceil(Math.abs(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; + + // Adding this no-op call to workaround a proguard related issue. + p.rLineTo(0, 0); + + 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/richpath/src/main/java/com/richpath/pathparser/PathParserCompat.java b/richpath/src/main/java/com/richpath/pathparser/PathParserCompat.java index c15d5a3..ddfd5f3 100644 --- a/richpath/src/main/java/com/richpath/pathparser/PathParserCompat.java +++ b/richpath/src/main/java/com/richpath/pathparser/PathParserCompat.java @@ -15,14 +15,12 @@ package com.richpath.pathparser; import android.graphics.Path; -import android.util.Log; import java.util.ArrayList; // This class is a duplicate from the PathParserCompat.java of frameworks/base, with slight // update on incompatible API like copyOfRange(). -class PathParserCompat { - private static final String LOGTAG = "PathParserCompat"; +public class PathParserCompat { // Copy from Arrays.copyOfRange() which is only available from API level 9. @@ -113,7 +111,7 @@ public static PathDataNode[] deepCopyNodes(PathDataNode[] source) { if (source == null) { return null; } - PathDataNode[] copy = new PathParserCompat.PathDataNode[source.length]; + PathDataNode[] copy = new PathDataNode[source.length]; for (int i = 0; i < source.length; i++) { copy[i] = new PathDataNode(source[i]); } @@ -143,6 +141,36 @@ public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) return true; } + /** + * @param nodes paths represented in an array of an array of PathDataNode + * @return whether the nodesFrom can morph into nodesTo + */ + public static boolean canMorph(PathDataNode[]... nodes) { + + for (PathDataNode[] pathDataNode : nodes) { + if (pathDataNode == null) { + return false; + } + } + + for (int i = 0; i < nodes.length - 1; i++) { + if (nodes[i].length != nodes[i + 1].length) { + return false; + } + } + + for (int i = 0; i < nodes.length - 1; i++) { + for (int j = 0; j < nodes[i].length; j++) { + if (nodes[i][j].type != nodes[i + 1][j].type + || nodes[i][j].params.length != nodes[i + 1][j].params.length) { + return false; + } + } + } + + return true; + } + /** * Update the target's data to match the source. * Before calling this, make sure canMorph(target, source) is true. @@ -289,456 +317,4 @@ private static void extract(String s, int start, ExtractFloatResult result) { // of the string. result.mEndPosition = currentIndex; } - - /** - * 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 { - /*package*/ - char type; - float[] params; - - PathDataNode(char type, float[] params) { - this.type = type; - this.params = params; - } - - PathDataNode(PathDataNode n) { - type = n.type; - params = copyOfRange(n.params, 0, n.params.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[6]; - char previousCommand = 'm'; - for (int i = 0; i < node.length; i++) { - addCommand(path, current, previousCommand, node[i].type, node[i].params); - previousCommand = node[i].type; - } - } - - /** - * The current PathDataNode will be interpolated between the - * nodeFrom and nodeTo according to the - * fraction. - * - * @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.params.length; i++) { - params[i] = nodeFrom.params[i] * (1 - fraction) - + nodeTo.params[i] * fraction; - } - } - - 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 currentSegmentStartX = current[4]; - float currentSegmentStartY = current[5]; - float reflectiveCtrlPointX; - float reflectiveCtrlPointY; - - switch (cmd) { - case 'z': - case 'Z': - path.close(); - // Path is closed here, but we need to move the pen to the - // closed position. So we cache the segment's starting position, - // and restore it here. - currentX = currentSegmentStartX; - currentY = currentSegmentStartY; - ctrlPointX = currentSegmentStartX; - ctrlPointY = currentSegmentStartY; - path.moveTo(currentX, currentY); - break; - 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) - currentX += val[k + 0]; - currentY += val[k + 1]; - if (k > 0) { - // According to the spec, if a moveto is followed by multiple - // pairs of coordinates, the subsequent pairs are treated as - // implicit lineto commands. - path.rLineTo(val[k + 0], val[k + 1]); - } else { - path.rMoveTo(val[k + 0], val[k + 1]); - currentSegmentStartX = currentX; - currentSegmentStartY = currentY; - } - break; - case 'M': // moveto - Start a new sub-path - currentX = val[k + 0]; - currentY = val[k + 1]; - if (k > 0) { - // According to the spec, if a moveto is followed by multiple - // pairs of coordinates, the subsequent pairs are treated as - // implicit lineto commands. - path.lineTo(val[k + 0], val[k + 1]); - } else { - path.moveTo(val[k + 0], val[k + 1]); - currentSegmentStartX = currentX; - currentSegmentStartY = currentY; - } - 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 '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; - current[4] = currentSegmentStartX; - current[5] = currentSegmentStartY; - } - - 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 = (int) Math.ceil(Math.abs(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; - - // Adding this no-op call to workaround a proguard related issue. - p.rLineTo(0, 0); - - 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/richpath/src/main/java/com/richpath/util/PathUtils.java b/richpath/src/main/java/com/richpath/util/PathUtils.java index 006783c..ddd24a4 100644 --- a/richpath/src/main/java/com/richpath/util/PathUtils.java +++ b/richpath/src/main/java/com/richpath/util/PathUtils.java @@ -4,6 +4,7 @@ import android.graphics.Path; import android.graphics.RectF; +import com.richpath.pathparser.PathDataNode; import com.richpath.pathparser.PathParser; /** @@ -102,9 +103,8 @@ public static void setPathScaleY(Path path, float scale) { setPathScaleY(path, scale, rect.centerX(), rect.centerY()); } - public static void setPathData(Path path, String pathData) { + public static void setPathDataNodes(Path path, PathDataNode[] pathDataNodes) { path.reset(); - PathParser.createPathFromPathData(path, pathData); + PathDataNode.nodesToPath(pathDataNodes, path); } - }