• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.N;
4 
5 import android.graphics.Path;
6 import android.util.Log;
7 import android.util.PathParser;
8 import android.util.PathParser.PathData;
9 import java.util.ArrayList;
10 import java.util.Arrays;
11 import org.robolectric.annotation.Implementation;
12 import org.robolectric.annotation.Implements;
13 
14 @Implements(value = PathParser.class, minSdk = N, isInAndroidSdk = false)
15 public class ShadowPathParser {
16 
17   static final String LOGTAG = ShadowPathParser.class.getSimpleName();
18 
19   @Implementation
createPathFromPathData(String pathData)20   protected static Path createPathFromPathData(String pathData) {
21     Path path = new Path();
22     PathDataNode[] nodes = createNodesFromPathData(pathData);
23     if (nodes != null) {
24       PathDataNode.nodesToPath(nodes, path);
25       return path;
26     }
27     return null;
28   }
29 
createNodesFromPathData(String pathData)30   public static PathDataNode[] createNodesFromPathData(String pathData) {
31     if (pathData == null) {
32       return null;
33     }
34     int start = 0;
35     int end = 1;
36 
37     ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
38     while (end < pathData.length()) {
39       end = nextStart(pathData, end);
40       String s = pathData.substring(start, end).trim();
41       if (s.length() > 0) {
42         float[] val = getFloats(s);
43         addNode(list, s.charAt(0), val);
44       }
45 
46       start = end;
47       end++;
48     }
49     if ((end - start) == 1 && start < pathData.length()) {
50       addNode(list, pathData.charAt(start), new float[0]);
51     }
52     return list.toArray(new PathDataNode[list.size()]);
53   }
54 
55   @Implementation
interpolatePathData( PathData outData, PathData fromData, PathData toData, float fraction)56   protected static boolean interpolatePathData(
57       PathData outData, PathData fromData, PathData toData, float fraction) {
58     return true;
59   }
60 
61   @Implementation
nCanMorph(long fromDataPtr, long toDataPtr)62   public static boolean nCanMorph(long fromDataPtr, long toDataPtr) {
63     return true;
64   }
65 
nextStart(String s, int end)66   private static int nextStart(String s, int end) {
67     char c;
68 
69     while (end < s.length()) {
70       c = s.charAt(end);
71       if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) {
72         return end;
73       }
74       end++;
75     }
76     return end;
77   }
78 
addNode(ArrayList<PathDataNode> list, char cmd, float[] val)79   private static void addNode(ArrayList<PathDataNode> list, char cmd, float[] val) {
80     list.add(new PathDataNode(cmd, val));
81   }
82 
83   private static class ExtractFloatResult {
84     // We need to return the position of the next separator and whether the
85     // next float starts with a '-'.
86     int mEndPosition;
87     boolean mEndWithNegSign;
88   }
89 
getFloats(String s)90   private static float[] getFloats(String s) {
91     if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
92       return new float[0];
93     }
94     try {
95       float[] results = new float[s.length()];
96       int count = 0;
97       int startPosition = 1;
98       int endPosition = 0;
99 
100       ExtractFloatResult result = new ExtractFloatResult();
101       int totalLength = s.length();
102 
103       // The startPosition should always be the first character of the
104       // current number, and endPosition is the character after the current
105       // number.
106       while (startPosition < totalLength) {
107         extract(s, startPosition, result);
108         endPosition = result.mEndPosition;
109 
110         if (startPosition < endPosition) {
111           results[count++] = Float.parseFloat(s.substring(startPosition, endPosition));
112         }
113 
114         if (result.mEndWithNegSign) {
115           // Keep the '-' sign with next number.
116           startPosition = endPosition;
117         } else {
118           startPosition = endPosition + 1;
119         }
120       }
121       return Arrays.copyOf(results, count);
122     } catch (NumberFormatException e) {
123       Log.e(LOGTAG, "error in parsing \"" + s + "\"");
124       throw e;
125     }
126   }
127 
extract(String s, int start, ExtractFloatResult result)128   private static void extract(String s, int start, ExtractFloatResult result) {
129     // Now looking for ' ', ',' or '-' from the start.
130     int currentIndex = start;
131     boolean foundSeparator = false;
132     result.mEndWithNegSign = false;
133     for (; currentIndex < s.length(); currentIndex++) {
134       char currentChar = s.charAt(currentIndex);
135       switch (currentChar) {
136         case ' ':
137         case ',':
138           foundSeparator = true;
139           break;
140         case '-':
141           if (currentIndex != start) {
142             foundSeparator = true;
143             result.mEndWithNegSign = true;
144           }
145           break;
146       }
147       if (foundSeparator) {
148         break;
149       }
150     }
151     // When there is nothing found, then we put the end position to the end
152     // of the string.
153     result.mEndPosition = currentIndex;
154   }
155 
156   public static class PathDataNode {
157     private char mType;
158     private float[] mParams;
159 
PathDataNode(char type, float[] params)160     private PathDataNode(char type, float[] params) {
161       mType = type;
162       mParams = params;
163     }
164 
165     /**
166      * Convert an array of PathDataNode to Path.
167      *
168      * @param node The source array of PathDataNode.
169      * @param path The target Path object.
170      */
nodesToPath(PathDataNode[] node, Path path)171     public static void nodesToPath(PathDataNode[] node, Path path) {
172       float[] current = new float[4];
173       char previousCommand = 'm';
174       for (int i = 0; i < node.length; i++) {
175         addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
176         previousCommand = node[i].mType;
177       }
178     }
179 
180     /**
181      * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and <code>
182      * nodeTo</code> according to the <code>fraction</code>.
183      *
184      * @param nodeFrom The start value as a PathDataNode.
185      * @param nodeTo The end value as a PathDataNode
186      * @param fraction The fraction to interpolate.
187      */
interpolatePathDataNode( PathDataNode nodeFrom, PathDataNode nodeTo, float fraction)188     public void interpolatePathDataNode(
189         PathDataNode nodeFrom, PathDataNode nodeTo, float fraction) {
190       for (int i = 0; i < nodeFrom.mParams.length; i++) {
191         mParams[i] = nodeFrom.mParams[i] * (1 - fraction) + nodeTo.mParams[i] * fraction;
192       }
193     }
194 
addCommand( Path path, float[] current, char previousCmd, char cmd, float[] val)195     private static void addCommand(
196         Path path, float[] current, char previousCmd, char cmd, float[] val) {
197 
198       int incr = 2;
199       float currentX = current[0];
200       float currentY = current[1];
201       float ctrlPointX = current[2];
202       float ctrlPointY = current[3];
203       float reflectiveCtrlPointX;
204       float reflectiveCtrlPointY;
205 
206       switch (cmd) {
207         case 'z':
208         case 'Z':
209           path.close();
210           return;
211         case 'm':
212         case 'M':
213         case 'l':
214         case 'L':
215         case 't':
216         case 'T':
217           incr = 2;
218           break;
219         case 'h':
220         case 'H':
221         case 'v':
222         case 'V':
223           incr = 1;
224           break;
225         case 'c':
226         case 'C':
227           incr = 6;
228           break;
229         case 's':
230         case 'S':
231         case 'q':
232         case 'Q':
233           incr = 4;
234           break;
235         case 'a':
236         case 'A':
237           incr = 7;
238           break;
239       }
240       for (int k = 0; k < val.length; k += incr) {
241         switch (cmd) {
242           case 'm': // moveto - Start a new sub-path (relative)
243             path.rMoveTo(val[k + 0], val[k + 1]);
244             currentX += val[k + 0];
245             currentY += val[k + 1];
246             break;
247           case 'M': // moveto - Start a new sub-path
248             path.moveTo(val[k + 0], val[k + 1]);
249             currentX = val[k + 0];
250             currentY = val[k + 1];
251             break;
252           case 'l': // lineto - Draw a line from the current point (relative)
253             path.rLineTo(val[k + 0], val[k + 1]);
254             currentX += val[k + 0];
255             currentY += val[k + 1];
256             break;
257           case 'L': // lineto - Draw a line from the current point
258             path.lineTo(val[k + 0], val[k + 1]);
259             currentX = val[k + 0];
260             currentY = val[k + 1];
261             break;
262           case 'z': // closepath - Close the current subpath
263           case 'Z': // closepath - Close the current subpath
264             path.close();
265             break;
266           case 'h': // horizontal lineto - Draws a horizontal line (relative)
267             path.rLineTo(val[k + 0], 0);
268             currentX += val[k + 0];
269             break;
270           case 'H': // horizontal lineto - Draws a horizontal line
271             path.lineTo(val[k + 0], currentY);
272             currentX = val[k + 0];
273             break;
274           case 'v': // vertical lineto - Draws a vertical line from the current point (r)
275             path.rLineTo(0, val[k + 0]);
276             currentY += val[k + 0];
277             break;
278           case 'V': // vertical lineto - Draws a vertical line from the current point
279             path.lineTo(currentX, val[k + 0]);
280             currentY = val[k + 0];
281             break;
282           case 'c': // curveto - Draws a cubic Bézier curve (relative)
283             path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]);
284 
285             ctrlPointX = currentX + val[k + 2];
286             ctrlPointY = currentY + val[k + 3];
287             currentX += val[k + 4];
288             currentY += val[k + 5];
289 
290             break;
291           case 'C': // curveto - Draws a cubic Bézier curve
292             path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]);
293             currentX = val[k + 4];
294             currentY = val[k + 5];
295             ctrlPointX = val[k + 2];
296             ctrlPointY = val[k + 3];
297             break;
298           case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
299             reflectiveCtrlPointX = 0;
300             reflectiveCtrlPointY = 0;
301             if (previousCmd == 'c'
302                 || previousCmd == 's'
303                 || previousCmd == 'C'
304                 || previousCmd == 'S') {
305               reflectiveCtrlPointX = currentX - ctrlPointX;
306               reflectiveCtrlPointY = currentY - ctrlPointY;
307             }
308             path.rCubicTo(
309                 reflectiveCtrlPointX,
310                 reflectiveCtrlPointY,
311                 val[k + 0],
312                 val[k + 1],
313                 val[k + 2],
314                 val[k + 3]);
315 
316             ctrlPointX = currentX + val[k + 0];
317             ctrlPointY = currentY + val[k + 1];
318             currentX += val[k + 2];
319             currentY += val[k + 3];
320             break;
321           case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
322             reflectiveCtrlPointX = currentX;
323             reflectiveCtrlPointY = currentY;
324             if (previousCmd == 'c'
325                 || previousCmd == 's'
326                 || previousCmd == 'C'
327                 || previousCmd == 'S') {
328               reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
329               reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
330             }
331             path.cubicTo(
332                 reflectiveCtrlPointX,
333                 reflectiveCtrlPointY,
334                 val[k + 0],
335                 val[k + 1],
336                 val[k + 2],
337                 val[k + 3]);
338             ctrlPointX = val[k + 0];
339             ctrlPointY = val[k + 1];
340             currentX = val[k + 2];
341             currentY = val[k + 3];
342             break;
343           case 'q': // Draws a quadratic Bézier (relative)
344             path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
345             ctrlPointX = currentX + val[k + 0];
346             ctrlPointY = currentY + val[k + 1];
347             currentX += val[k + 2];
348             currentY += val[k + 3];
349             break;
350           case 'Q': // Draws a quadratic Bézier
351             path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
352             ctrlPointX = val[k + 0];
353             ctrlPointY = val[k + 1];
354             currentX = val[k + 2];
355             currentY = val[k + 3];
356             break;
357           case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
358             reflectiveCtrlPointX = 0;
359             reflectiveCtrlPointY = 0;
360             if (previousCmd == 'q'
361                 || previousCmd == 't'
362                 || previousCmd == 'Q'
363                 || previousCmd == 'T') {
364               reflectiveCtrlPointX = currentX - ctrlPointX;
365               reflectiveCtrlPointY = currentY - ctrlPointY;
366             }
367             path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k + 0], val[k + 1]);
368             ctrlPointX = currentX + reflectiveCtrlPointX;
369             ctrlPointY = currentY + reflectiveCtrlPointY;
370             currentX += val[k + 0];
371             currentY += val[k + 1];
372             break;
373           case 'T': // Draws a quadratic Bézier curve (reflective control point)
374             reflectiveCtrlPointX = currentX;
375             reflectiveCtrlPointY = currentY;
376             if (previousCmd == 'q'
377                 || previousCmd == 't'
378                 || previousCmd == 'Q'
379                 || previousCmd == 'T') {
380               reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
381               reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
382             }
383             path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k + 0], val[k + 1]);
384             ctrlPointX = reflectiveCtrlPointX;
385             ctrlPointY = reflectiveCtrlPointY;
386             currentX = val[k + 0];
387             currentY = val[k + 1];
388             break;
389           case 'a': // Draws an elliptical arc
390             // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
391             drawArc(
392                 path,
393                 currentX,
394                 currentY,
395                 val[k + 5] + currentX,
396                 val[k + 6] + currentY,
397                 val[k + 0],
398                 val[k + 1],
399                 val[k + 2],
400                 val[k + 3] != 0,
401                 val[k + 4] != 0);
402             currentX += val[k + 5];
403             currentY += val[k + 6];
404             ctrlPointX = currentX;
405             ctrlPointY = currentY;
406             break;
407           case 'A': // Draws an elliptical arc
408             drawArc(
409                 path,
410                 currentX,
411                 currentY,
412                 val[k + 5],
413                 val[k + 6],
414                 val[k + 0],
415                 val[k + 1],
416                 val[k + 2],
417                 val[k + 3] != 0,
418                 val[k + 4] != 0);
419             currentX = val[k + 5];
420             currentY = val[k + 6];
421             ctrlPointX = currentX;
422             ctrlPointY = currentY;
423             break;
424         }
425         previousCmd = cmd;
426       }
427       current[0] = currentX;
428       current[1] = currentY;
429       current[2] = ctrlPointX;
430       current[3] = ctrlPointY;
431     }
432 
drawArc( Path p, float x0, float y0, float x1, float y1, float a, float b, float theta, boolean isMoreThanHalf, boolean isPositiveArc)433     private static void drawArc(
434         Path p,
435         float x0,
436         float y0,
437         float x1,
438         float y1,
439         float a,
440         float b,
441         float theta,
442         boolean isMoreThanHalf,
443         boolean isPositiveArc) {
444 
445       /* Convert rotation angle from degrees to radians */
446       double thetaD = Math.toRadians(theta);
447       /* Pre-compute rotation matrix entries */
448       double cosTheta = Math.cos(thetaD);
449       double sinTheta = Math.sin(thetaD);
450       /* Transform (x0, y0) and (x1, y1) into unit space */
451       /* using (inverse) rotation, followed by (inverse) scale */
452       double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
453       double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
454       double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
455       double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
456 
457       /* Compute differences and averages */
458       double dx = x0p - x1p;
459       double dy = y0p - y1p;
460       double xm = (x0p + x1p) / 2;
461       double ym = (y0p + y1p) / 2;
462       /* Solve for intersecting unit circles */
463       double dsq = dx * dx + dy * dy;
464       if (dsq == 0.0) {
465         Log.w(LOGTAG, " Points are coincident");
466         return; /* Points are coincident */
467       }
468       double disc = 1.0 / dsq - 1.0 / 4.0;
469       if (disc < 0.0) {
470         Log.w(LOGTAG, "Points are too far apart " + dsq);
471         float adjust = (float) (Math.sqrt(dsq) / 1.99999);
472         drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta, isMoreThanHalf, isPositiveArc);
473         return; /* Points are too far apart */
474       }
475       double s = Math.sqrt(disc);
476       double sdx = s * dx;
477       double sdy = s * dy;
478       double cx;
479       double cy;
480       if (isMoreThanHalf == isPositiveArc) {
481         cx = xm - sdy;
482         cy = ym + sdx;
483       } else {
484         cx = xm + sdy;
485         cy = ym - sdx;
486       }
487 
488       double eta0 = Math.atan2((y0p - cy), (x0p - cx));
489 
490       double eta1 = Math.atan2((y1p - cy), (x1p - cx));
491 
492       double sweep = (eta1 - eta0);
493       if (isPositiveArc != (sweep >= 0)) {
494         if (sweep > 0) {
495           sweep -= 2 * Math.PI;
496         } else {
497           sweep += 2 * Math.PI;
498         }
499       }
500 
501       cx *= a;
502       cy *= b;
503       double tcx = cx;
504       cx = cx * cosTheta - cy * sinTheta;
505       cy = tcx * sinTheta + cy * cosTheta;
506 
507       arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
508     }
509 
510     /**
511      * Converts an arc to cubic Bezier segments and records them in p.
512      *
513      * @param p The target for the cubic Bezier segments
514      * @param cx The x coordinate center of the ellipse
515      * @param cy The y coordinate center of the ellipse
516      * @param a The radius of the ellipse in the horizontal direction
517      * @param b The radius of the ellipse in the vertical direction
518      * @param e1x E(eta1) x coordinate of the starting point of the arc
519      * @param e1y E(eta2) y coordinate of the starting point of the arc
520      * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
521      * @param start The start angle of the arc on the ellipse
522      * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
523      */
arcToBezier( Path p, double cx, double cy, double a, double b, double e1x, double e1y, double theta, double start, double sweep)524     private static void arcToBezier(
525         Path p,
526         double cx,
527         double cy,
528         double a,
529         double b,
530         double e1x,
531         double e1y,
532         double theta,
533         double start,
534         double sweep) {
535       // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
536       // and http://www.spaceroots.org/documents/ellipse/node22.html
537 
538       // Maximum of 45 degrees per cubic Bezier segment
539       int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI));
540 
541       double eta1 = start;
542       double cosTheta = Math.cos(theta);
543       double sinTheta = Math.sin(theta);
544       double cosEta1 = Math.cos(eta1);
545       double sinEta1 = Math.sin(eta1);
546       double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
547       double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
548 
549       double anglePerSegment = sweep / numSegments;
550       for (int i = 0; i < numSegments; i++) {
551         double eta2 = eta1 + anglePerSegment;
552         double sinEta2 = Math.sin(eta2);
553         double cosEta2 = Math.cos(eta2);
554         double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
555         double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
556         double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
557         double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
558         double tanDiff2 = Math.tan((eta2 - eta1) / 2);
559         double alpha = Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
560         double q1x = e1x + alpha * ep1x;
561         double q1y = e1y + alpha * ep1y;
562         double q2x = e2x - alpha * ep2x;
563         double q2y = e2y - alpha * ep2y;
564 
565         p.cubicTo((float) q1x, (float) q1y, (float) q2x, (float) q2y, (float) e2x, (float) e2y);
566         eta1 = eta2;
567         e1x = e2x;
568         e1y = e2y;
569         ep1x = ep2x;
570         ep1y = ep2y;
571       }
572     }
573   }
574 
575   @Implementation
nCreatePathDataFromString(String pathString, int stringLength)576   protected static long nCreatePathDataFromString(String pathString, int stringLength) {
577     return 1;
578   }
579 
580   @Implementation
nCreateEmptyPathData()581   protected static long nCreateEmptyPathData() {
582     return 1;
583   }
584 
585   @Implementation
nCreatePathData(long nativePtr)586   protected static long nCreatePathData(long nativePtr) {
587     return 1;
588   }
589 }
590