Add path for PathInterpolator

This commit is contained in:
seven332 committed 2015-03-21 16:51:08 +08:00
1 parent 182d7b2b96
commit 3b31fbc1fb
3 files changed
+1176 -124

No files matched your search

@@ -31,7 +31,6 @@ import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.PathInterpolator;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -0,0 +1,648 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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
* 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 com.hippo.animation;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
class IpPathParser {
static final String LOGTAG = IpPathParser.class.getSimpleName();
/**
* @param pathData The string representing a path, the same as "d" string in svg file.
* @return the generated Path object.
*/
public static PathInterpolator.Path createPathFromPathData(String pathData) {
PathInterpolator.Path path = new PathInterpolator.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) {
if (pathData == null) {
return null;
}
int start = 0;
int end = 1;
ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
while (end < pathData.length()) {
end = nextStart(pathData, end);
String s = pathData.substring(start, end).trim();
if (s.length() > 0) {
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()]);
}
/**
* @param source The array of PathDataNode to be duplicated.
* @return a deep copy of the <code>source</code>.
*/
public static PathDataNode[] deepCopyNodes(PathDataNode[] source) {
if (source == null) {
return null;
}
PathDataNode[] copy = new 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;
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<PathDataNode> list, char cmd, float[] val) {
list.add(new PathDataNode(cmd, val));
}
private static class ExtractFloatResult {
// We need to return the position of the next separator and whether the
// next float starts with a '-'.
int mEndPosition;
boolean mEndWithNegSign;
}
/**
* 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[] results = new float[s.length()];
int count = 0;
int startPosition = 1;
int endPosition = 0;
ExtractFloatResult result = new ExtractFloatResult();
int totalLength = s.length();
// The startPosition should always be the first character of the
// current number, and endPosition is the character after the current
// number.
while (startPosition < totalLength) {
extract(s, startPosition, result);
endPosition = result.mEndPosition;
if (startPosition < endPosition) {
results[count++] = Float.parseFloat(
s.substring(startPosition, endPosition));
}
if (result.mEndWithNegSign) {
// Keep the '-' sign with next number.
startPosition = endPosition;
} else {
startPosition = endPosition + 1;
}
}
return Arrays.copyOf(results, count);
} catch (NumberFormatException e) {
Log.e(LOGTAG, "error in parsing \"" + s + "\"");
throw e;
}
}
/**
* Calculate the position of the next comma or space or negative sign
* @param s the string to search
* @param start the position to start searching
* @param result the result of the extraction, including the position of the
* the starting position of next number, whether it is ending with a '-'.
*/
private static void extract(String s, int start, ExtractFloatResult result) {
// Now looking for ' ', ',' or '-' from the start.
int currentIndex = start;
boolean foundSeparator = false;
result.mEndWithNegSign = false;
for (; currentIndex < s.length(); currentIndex++) {
char currentChar = s.charAt(currentIndex);
switch (currentChar) {
case ' ':
case ',':
foundSeparator = true;
break;
case '-':
if (currentIndex != start) {
foundSeparator = true;
result.mEndWithNegSign = true;
}
break;
}
if (foundSeparator) {
break;
}
}
// When there is nothing found, then we put the end position to the end
// 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 {
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);
}
/**
* 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, PathInterpolator.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;
}
}
/**
* 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(PathInterpolator.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(PathInterpolator.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(PathInterpolator.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;
}
}
}
}
@@ -25,6 +25,9 @@ import android.view.animation.Interpolator;
import com.hippo.ehviewer.R;
import java.util.LinkedList;
import java.util.List;
/**
* An interpolator that can traverse a Path that extends from <code>Point</code>
* <code>(0, 0)</code> to <code>(1, 1)</code>. The x coordinate along the <code>Path</code>
@@ -45,7 +48,6 @@ public class PathInterpolator implements Interpolator {
// This governs how accurate the approximation of the Path is.
private static final float PRECISION = 0.002f;
private static final float ERROR_SQUARED = PRECISION * PRECISION;
private float[] mX; // x coordinates in the line
@@ -57,11 +59,9 @@ public class PathInterpolator implements Interpolator {
*
* @param path The <code>Path</code> to use to make the line representing the interpolator.
*/
/* // TODO I think I do not need it
public PathInterpolator(Path path) {
initPath(path);
}
*/
/**
* Create an interpolator for a quadratic Bezier curve. The end points
@@ -100,7 +100,7 @@ public class PathInterpolator implements Interpolator {
a = res.obtainAttributes(attrs, R.styleable.PathInterpolator);
}
parseInterpolatorFromTypeArray(a);
//setChangingConfiguration(a.getChangingConfigurations());
a.recycle();
}
@@ -108,15 +108,13 @@ public class PathInterpolator implements Interpolator {
// 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)) {
/* TODO I think I do not need it
String pathData = a.getString(R.styleable.PathInterpolator_pathData);
Path path = PathParser.createPathFromPathData(pathData);
Path path = IpPathParser.createPathFromPathData(pathData);
if (path == null) {
throw new InflateException("The path is null, which is created"
+ " from " + pathData);
}
initPath(path);
*/
} else {
if (!a.hasValue(R.styleable.PathInterpolator_controlX1)) {
throw new InflateException("pathInterpolator requires the controlX1 attribute");
@@ -145,16 +143,22 @@ public class PathInterpolator implements Interpolator {
}
private void initQuad(float controlX, float controlY) {
float[] points = bezierApproximate(new float[]{controlX, controlY}, ERROR_SQUARED, false);
loadPointComponents(points);
Path path = new Path();
path.moveTo(0, 0);
path.quadTo(controlX, controlY, 1f, 1f);
initPath(path);
}
private void initCubic(float x1, float y1, float x2, float y2) {
float[] points = bezierApproximate(new float[]{x1, y1, x2, y2}, ERROR_SQUARED, true);
loadPointComponents(points);
Path path = new Path();
path.moveTo(0, 0);
path.cubicTo(x1, y1, x2, y2, 1f, 1f);
initPath(path);
}
private void loadPointComponents(float[] pointComponents) {
private void initPath(Path path) {
float[] pointComponents = path.approximate(PRECISION);
int numPoints = pointComponents.length / 3;
if (pointComponents[1] != 0 || pointComponents[2] != 0
|| pointComponents[pointComponents.length - 2] != 1
@@ -228,6 +232,484 @@ public class PathInterpolator implements Interpolator {
return startY + (fraction * (endY - startY));
}
private static final class PointMap {
private int size = 0;
private Link firstLink;
private Link lastLink;
float totalLength = 0f;
private static final class Link {
/** It is only for bezier **/
float t;
float x;
float y;
/** It should be Length first **/
float f = -1;
private Link previous, next;
Link(float t, float x, float y, float f, Link p, Link n) {
this.t = t;
this.x = x;
this.y = y;
this.f = f;
this.previous = p;
this.next = n;
}
}
public static final class Iterator {
final PointMap parent;
Link link;
private Iterator(PointMap p, Link l) {
parent = p;
link = l;
}
boolean hasNext() {
return link.next != null;
}
Iterator nextIterator() {
if (hasNext()) {
return new Iterator(parent, link.next);
} else {
return null;
}
}
// Move iterator to the new data
void addBefore(float t, float x, float y, float f) {
Link p = link.previous;
Link l = new Link(t, x, y, f, p, link);
link.previous = l;
p.next = l;
link = l;
parent.size++;
}
}
public void add(float t, float x, float y, float f) {
Link link = new Link(t, x, y, f, null, null);
if (lastLink != null) {
link.previous = lastLink;
lastLink.next = link;
}
lastLink = link;
if (firstLink == null) {
firstLink = link;
}
size++;
}
// Get a iterator at first data or null
public Iterator iterator() {
if (firstLink != null) {
return new Iterator(this, firstLink);
} else {
return null;
}
}
public Iterator lastIterator() {
if (lastLink != null) {
return new Iterator(this, lastLink);
} else {
return null;
}
}
public float[] generate() {
int index = 0;
float[] result = new float[size * 3];
for (Link link = firstLink; link != null; link = link.next) {
result[index++] = link.f / totalLength;
result[index++] = link.x;
result[index++] = link.y;
}
return result;
}
}
public static class Path {
private List<PathAction> mActionList = new LinkedList<>();
/** startX, startY, length**/
private float[] mTemp = new float[2];
{
mTemp[0] = 0f;
mTemp[1] = 0f;
}
public void close() {
// Empty
}
/**
* Set the beginning of the next contour to the point (x,y).
*
* @param x The x-coordinate of the start of a new contour
* @param y The y-coordinate of the start of a new contour
*/
public void moveTo(float x, float y) {
mActionList.add(new MoveAction(x, y));
}
/**
* Set the beginning of the next contour relative to the last point on the
* previous contour. If there is no previous contour, this is treated the
* same as moveTo().
*
* @param dx The amount to add to the x-coordinate of the end of the
* previous contour, to specify the start of a new contour
* @param dy The amount to add to the y-coordinate of the end of the
* previous contour, to specify the start of a new contour
*/
public void rMoveTo(float dx, float dy) {
mActionList.add(new RMoveAction(dx, dy));
}
/**
* Add a line from the last point to the specified point (x,y).
* If no moveTo() call has been made for this contour, the first point is
* automatically set to (0,0).
*
* @param x The x-coordinate of the end of a line
* @param y The y-coordinate of the end of a line
*/
public void lineTo(float x, float y) {
mActionList.add(new LineToAction(x, y));
}
/**
* Same as lineTo, but the coordinates are considered relative to the last
* point on this contour. If there is no previous point, then a moveTo(0,0)
* is inserted automatically.
*
* @param dx The amount to add to the x-coordinate of the previous point on
* this contour, to specify a line
* @param dy The amount to add to the y-coordinate of the previous point on
* this contour, to specify a line
*/
public void rLineTo(float dx, float dy) {
mActionList.add(new RLineToAction(dx, dy));
}
/**
* Add a quadratic bezier from the last point, approaching control point
* (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
* this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the control point on a quadratic curve
* @param y1 The y-coordinate of the control point on a quadratic curve
* @param x2 The x-coordinate of the end point on a quadratic curve
* @param y2 The y-coordinate of the end point on a quadratic curve
*/
public void quadTo(float x1, float y1, float x2, float y2) {
mActionList.add(new QuadToAction(x1, y1, x2, y2));
}
/**
* Same as quadTo, but the coordinates are considered relative to the last
* point on this contour. If there is no previous point, then a moveTo(0,0)
* is inserted automatically.
*
* @param dx1 The amount to add to the x-coordinate of the last point on
* this contour, for the control point of a quadratic curve
* @param dy1 The amount to add to the y-coordinate of the last point on
* this contour, for the control point of a quadratic curve
* @param dx2 The amount to add to the x-coordinate of the last point on
* this contour, for the end point of a quadratic curve
* @param dy2 The amount to add to the y-coordinate of the last point on
* this contour, for the end point of a quadratic curve
*/
public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
mActionList.add(new RQuadToAction(dx1, dy1, dx2, dy2));
}
/**
* Add a cubic bezier from the last point, approaching control points
* (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
* made for this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the 1st control point on a cubic curve
* @param y1 The y-coordinate of the 1st control point on a cubic curve
* @param x2 The x-coordinate of the 2nd control point on a cubic curve
* @param y2 The y-coordinate of the 2nd control point on a cubic curve
* @param x3 The x-coordinate of the end point on a cubic curve
* @param y3 The y-coordinate of the end point on a cubic curve
*/
public void cubicTo(float x1, float y1, float x2, float y2,
float x3, float y3) {
mActionList.add(new CubicToAction(x1, y1, x2, y2, x3, y3));
}
/**
* Same as cubicTo, but the coordinates are considered relative to the
* current point on this contour. If there is no previous point, then a
* moveTo(0,0) is inserted automatically.
*/
public void rCubicTo(float x1, float y1, float x2, float y2,
float x3, float y3) {
mActionList.add(new RCubicToAction(x1, y1, x2, y2, x3, y3));
}
/**
* Approximate the <code>Path</code> with a series of line segments.
* This returns float[] with the array containing point components.
* There are three components for each point, in order:
* <ul>
* <li>Fraction along the length of the path that the point resides</li>
* <li>The x coordinate of the point</li>
* <li>The y coordinate of the point</li>
* </ul>
* <p>Two points may share the same fraction along its length when there is
* a move action within the Path.</p>
*
* @param acceptableError The acceptable error for a line on the
* Path. Typically this would be 0.5 so that
* the error is less than half a pixel.
* @return An array of components for points approximating the Path.
*/
public float[] approximate(float acceptableError) {
PointMap pm = new PointMap();
for (PathAction pa : mActionList) {
pa.approximate(pm, mTemp, acceptableError);
}
return pm.generate();
}
}
private abstract static class PathAction {
public abstract void approximate(PointMap pointMap, float[] startPoint, float acceptableError);
}
private static class MoveAction extends PathAction {
private float mX;
private float mY;
public MoveAction(float x, float y) {
mX = x;
mY = y;
}
@Override
public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) {
float x = mX;
float y = mY;
pointMap.add(0f, x, y, pointMap.totalLength);
startPoint[0] = x;
startPoint[1] = y;
}
}
private static class RMoveAction extends PathAction {
private float mDx;
private float mDy;
public RMoveAction(float dx, float dy) {
mDx = dx;
mDy = dy;
}
@Override
public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) {
float x = startPoint[0] + mDx;
float y = startPoint[1] + mDy;
pointMap.add(0f, x, y, pointMap.totalLength);
startPoint[0] = x;
startPoint[1] = y;
}
}
private static class LineToAction extends PathAction {
private float mX;
private float mY;
public LineToAction(float x, float y) {
mX = x;
mY = y;
}
@Override
public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) {
if (pointMap.size == 0) {
// No start point in map, just add it
pointMap.add(0f, startPoint[0], startPoint[1], 0f);
}
float x = mX;
float y = mY;
float length = dist(startPoint[0], startPoint[1], x, y) +
pointMap.totalLength;
pointMap.add(0f, x, y, length);
pointMap.totalLength = length;
startPoint[0] = x;
startPoint[1] = y;
}
}
private static class RLineToAction extends PathAction {
private float mDx;
private float mDy;
public RLineToAction(float dx, float dy) {
mDx = dx;
mDy = dy;
}
@Override
public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) {
if (pointMap.size == 0) {
// No start point in map, just add it
pointMap.add(0f, startPoint[0], startPoint[1], 0f);
}
float x = startPoint[0] + mDx;
float y = startPoint[1] + mDy;
float length = (float) Math.hypot(mDx, mDy) +
pointMap.totalLength;
pointMap.add(0f, x, y, length);
pointMap.totalLength = length;
startPoint[0] = x;
startPoint[1] = y;
}
}
private static class QuadToAction extends PathAction {
private float mX1;
private float mY1;
private float mX2;
private float mY2;
public QuadToAction(float x1, float y1, float x2, float y2) {
mX1 = x1;
mY1 = y1;
mX2 = x2;
mY2 = y2;
}
@Override
public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) {
float[] control = new float[]{
startPoint[0], startPoint[1],
mX1, mY1,
mX2, mY2
};
bezierApproximate(pointMap, control, acceptableError, false);
startPoint[0] = control[0];
startPoint[1] = control[1];
}
}
private static class RQuadToAction extends PathAction {
private float mDx1;
private float mDy1;
private float mDx2;
private float mDy2;
public RQuadToAction(float dx1, float dy1, float dx2, float dy2) {
mDx1 = dx1;
mDy1 = dy1;
mDx2 = dx2;
mDy2 = dy2;
}
@Override
public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) {
float startX = startPoint[0];
float startY = startPoint[1];
float[] control = new float[]{
startX, startY,
startX + mDx1, startY + mDy1,
startX + mDx2, startY + mDy2
};
bezierApproximate(pointMap, control, acceptableError, false);
startPoint[0] = control[0];
startPoint[1] = control[1];
}
}
private static class CubicToAction extends PathAction {
private float mX1;
private float mY1;
private float mX2;
private float mY2;
private float mX3;
private float mY3;
public CubicToAction(float x1, float y1, float x2, float y2, float x3, float y3) {
mX1 = x1;
mY1 = y1;
mX2 = x2;
mY2 = y2;
mX3 = x3;
mY3 = y3;
}
@Override
public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) {
float[] control = new float[]{
startPoint[0], startPoint[1],
mX1, mY1,
mX2, mY2,
mX3, mY3
};
bezierApproximate(pointMap, control, acceptableError, true);
startPoint[0] = control[0];
startPoint[1] = control[1];
}
}
private static class RCubicToAction extends PathAction {
private float mDx1;
private float mDy1;
private float mDx2;
private float mDy2;
private float mDx3;
private float mDy3;
public RCubicToAction(float dx1, float dy1, float dx2, float dy2, float dx3, float dy3) {
mDx1 = dx1;
mDy1 = dy1;
mDx2 = dx2;
mDy2 = dy2;
mDx3 = dx3;
mDy3 = dy3;
}
@Override
public void approximate(PointMap pointMap, float[] startPoint, float acceptableError) {
float startX = startPoint[0];
float startY = startPoint[1];
float[] control = new float[]{
startX, startY,
startX + mDx1, startY + mDy1,
startX + mDx2, startY + mDy2,
startX + mDx3, startY + mDy3,
};
bezierApproximate(pointMap, control, acceptableError, true);
startPoint[0] = control[0];
startPoint[1] = control[1];
}
}
// Divides Bezier curves until linear interpolation is very close to accurate, using
// errorSquared as a metric. Cubic Bezier curves can have an inflection point that improperly
// short-circuit subdivision. If you imagine an S shape, the top and bottom points being the
@@ -235,14 +717,27 @@ public class PathInterpolator implements Interpolator {
// the point. It is clearly not the case that we can linearly interpolate at that point.
// doubleCheckDivision forces a second examination between subdivisions to ensure that linear
// interpolation works.
private static float[] bezierApproximate(float[] control, float errorSquared,
boolean isCubic) {
PointMap pointMap = new PointMap();
private static void bezierApproximate(PointMap pointMap, float[] control,
float acceptableError, boolean isCubic) {
float errorSquared = acceptableError * acceptableError;
pointMap.add(0f, 0f, 0f);
pointMap.add(1f, 1f, 1f);
if (pointMap.size == 0) {
// No start point in map, just add it
pointMap.add(0f, control[0], control[1], 0f);
}
PointMap.Iterator iter = pointMap.iterator();
PointMap.Iterator iter = pointMap.lastIterator();
// Make sure start t is 0
iter.link.t = 0f;
// Add end point
if (isCubic) {
pointMap.add(1f, control[6], control[7], 0);
} else {
pointMap.add(1f, control[4], control[5], 0);
}
// Get next iterator
PointMap.Iterator next = iter.nextIterator();
boolean doubleCheckDivision = isCubic;
@@ -277,7 +772,8 @@ public class PathInterpolator implements Interpolator {
}
}
if (needsSubdivision) {
next.addBefore(midT, midPointX, midPointY);
// Add length soon
next.addBefore(midT, midPointX, midPointY, 0);
} else {
float length = dist(iterLink.x, iterLink.y, nextLink.x, nextLink.y);
iterLink.f = pointMap.totalLength;
@@ -289,15 +785,10 @@ public class PathInterpolator implements Interpolator {
next = next.nextIterator();
}
iter.link.f = pointMap.totalLength;
return pointMap.generate();
}
private static float dist(float x1, float y1, float x2, float y2) {
final float x = (x2 - x1);
final float y = (y2 - y1);
return (float) Math.hypot(x, y);
PointMap.Link iterLink = iter.link;
iterLink.f = pointMap.totalLength;
control[0] = iterLink.x;
control[1] = iterLink.y;
}
private static boolean isOutOfErrorSquared(float midX, float midY,
@@ -310,17 +801,17 @@ public class PathInterpolator implements Interpolator {
private static float bezierX(float t, float[] control, boolean isCubic) {
if (isCubic) {
return cubicCoordinateCalculation(t, 0f, control[0], control[2], 1f);
return cubicCoordinateCalculation(t, control[0], control[2], control[4], control[6]);
} else {
return quadraticCoordinateCalculation(t, 0f, control[0], 1f);
return quadraticCoordinateCalculation(t, control[0], control[2], control[4]);
}
}
private static float bezierY(float t, float[] control, boolean isCubic) {
if (isCubic) {
return cubicCoordinateCalculation(t, 0f, control[1], control[3], 1f);
return cubicCoordinateCalculation(t, control[1], control[3], control[5], control[7]);
} else {
return quadraticCoordinateCalculation(t, 0f, control[1], 1f);
return quadraticCoordinateCalculation(t, control[1], control[3], control[5]);
}
}
@@ -339,96 +830,10 @@ public class PathInterpolator implements Interpolator {
+ (3 * oneMinusT * tSquared * p2) + (tCubed * p3);
}
private static final class PointMap {
private int size = 0;
private Link firstLink;
private Link lastLink;
float totalLength = 0f;
private static final class Link {
float t;
float x;
float y;
float f = -1;
private Link previous, next;
Link(float t, float x, float y, Link p, Link n) {
this.t = t;
this.x = x;
this.y = y;
this.previous = p;
this.next = n;
}
}
public static final class Iterator {
final PointMap parent;
Link link;
private Iterator(PointMap p, Link l) {
parent = p;
link = l;
}
boolean hasNext() {
return link.next != null;
}
Iterator nextIterator() {
if (hasNext()) {
return new Iterator(parent, link.next);
} else {
return null;
}
}
// Move iterator to the new data
void addBefore(float t, float x, float y) {
Link p = link.previous;
Link l = new Link(t, x, y, p, link);
link.previous = l;
p.next = l;
link = l;
parent.size++;
}
}
public void add(float t, float x, float y) {
Link link = new Link(t, x, y, null, null);
if (lastLink != null) {
link.previous = lastLink;
lastLink.next = link;
}
lastLink = link;
if (firstLink == null) {
firstLink = link;
}
size++;
}
// Get a iterator at first data or null
public Iterator iterator() {
if (firstLink != null) {
return new Iterator(this, firstLink);
} else {
return null;
}
}
public float[] generate() {
int index = 0;
float[] result = new float[size * 3];
for (Link link = firstLink; link != null; link = link.next) {
result[index++] = link.f / totalLength;
result[index++] = link.x;
result[index++] = link.y;
}
return result;
}
public static float dist(float x1, float y1, float x2, float y2) {
final float x = (x2 - x1);
final float y = (y2 - y1);
return (float) Math.hypot(x, y);
}
}