• 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 
PathDataNode(PathDataNode n)165     private PathDataNode(PathDataNode n) {
166       mType = n.mType;
167       mParams = Arrays.copyOf(n.mParams, n.mParams.length);
168     }
169 
170     /**
171      * Convert an array of PathDataNode to Path.
172      *
173      * @param node The source array of PathDataNode.
174      * @param path The target Path object.
175      */
nodesToPath(PathDataNode[] node, Path path)176     public static void nodesToPath(PathDataNode[] node, Path path) {
177       float[] current = new float[4];
178       char previousCommand = 'm';
179       for (int i = 0; i < node.length; i++) {
180         addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
181         previousCommand = node[i].mType;
182       }
183     }
184 
185     /**
186      * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and <code>
187      * nodeTo</code> according to the <code>fraction</code>.
188      *
189      * @param nodeFrom The start value as a PathDataNode.
190      * @param nodeTo The end value as a PathDataNode
191      * @param fraction The fraction to interpolate.
192      */
interpolatePathDataNode( PathDataNode nodeFrom, PathDataNode nodeTo, float fraction)193     public void interpolatePathDataNode(
194         PathDataNode nodeFrom, PathDataNode nodeTo, float fraction) {
195       for (int i = 0; i < nodeFrom.mParams.length; i++) {
196         mParams[i] = nodeFrom.mParams[i] * (1 - fraction) + nodeTo.mParams[i] * fraction;
197       }
198     }
199 
addCommand( Path path, float[] current, char previousCmd, char cmd, float[] val)200     private static void addCommand(
201         Path path, float[] current, char previousCmd, char cmd, float[] val) {
202 
203       int incr = 2;
204       float currentX = current[0];
205       float currentY = current[1];
206       float ctrlPointX = current[2];
207       float ctrlPointY = current[3];
208       float reflectiveCtrlPointX;
209       float reflectiveCtrlPointY;
210 
211       switch (cmd) {
212         case 'z':
213         case 'Z':
214           path.close();
215           return;
216         case 'm':
217         case 'M':
218         case 'l':
219         case 'L':
220         case 't':
221         case 'T':
222           incr = 2;
223           break;
224         case 'h':
225         case 'H':
226         case 'v':
227         case 'V':
228           incr = 1;
229           break;
230         case 'c':
231         case 'C':
232           incr = 6;
233           break;
234         case 's':
235         case 'S':
236         case 'q':
237         case 'Q':
238           incr = 4;
239           break;
240         case 'a':
241         case 'A':
242           incr = 7;
243           break;
244       }
245       for (int k = 0; k < val.length; k += incr) {
246         switch (cmd) {
247           case 'm': // moveto - Start a new sub-path (relative)
248             path.rMoveTo(val[k + 0], val[k + 1]);
249             currentX += val[k + 0];
250             currentY += val[k + 1];
251             break;
252           case 'M': // moveto - Start a new sub-path
253             path.moveTo(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 (relative)
258             path.rLineTo(val[k + 0], val[k + 1]);
259             currentX += val[k + 0];
260             currentY += val[k + 1];
261             break;
262           case 'L': // lineto - Draw a line from the current point
263             path.lineTo(val[k + 0], val[k + 1]);
264             currentX = val[k + 0];
265             currentY = val[k + 1];
266             break;
267           case 'z': // closepath - Close the current subpath
268           case 'Z': // closepath - Close the current subpath
269             path.close();
270             break;
271           case 'h': // horizontal lineto - Draws a horizontal line (relative)
272             path.rLineTo(val[k + 0], 0);
273             currentX += val[k + 0];
274             break;
275           case 'H': // horizontal lineto - Draws a horizontal line
276             path.lineTo(val[k + 0], currentY);
277             currentX = val[k + 0];
278             break;
279           case 'v': // vertical lineto - Draws a vertical line from the current point (r)
280             path.rLineTo(0, val[k + 0]);
281             currentY += val[k + 0];
282             break;
283           case 'V': // vertical lineto - Draws a vertical line from the current point
284             path.lineTo(currentX, val[k + 0]);
285             currentY = val[k + 0];
286             break;
287           case 'c': // curveto - Draws a cubic Bézier curve (relative)
288             path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]);
289 
290             ctrlPointX = currentX + val[k + 2];
291             ctrlPointY = currentY + val[k + 3];
292             currentX += val[k + 4];
293             currentY += val[k + 5];
294 
295             break;
296           case 'C': // curveto - Draws a cubic Bézier curve
297             path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]);
298             currentX = val[k + 4];
299             currentY = val[k + 5];
300             ctrlPointX = val[k + 2];
301             ctrlPointY = val[k + 3];
302             break;
303           case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
304             reflectiveCtrlPointX = 0;
305             reflectiveCtrlPointY = 0;
306             if (previousCmd == 'c'
307                 || previousCmd == 's'
308                 || previousCmd == 'C'
309                 || previousCmd == 'S') {
310               reflectiveCtrlPointX = currentX - ctrlPointX;
311               reflectiveCtrlPointY = currentY - ctrlPointY;
312             }
313             path.rCubicTo(
314                 reflectiveCtrlPointX,
315                 reflectiveCtrlPointY,
316                 val[k + 0],
317                 val[k + 1],
318                 val[k + 2],
319                 val[k + 3]);
320 
321             ctrlPointX = currentX + val[k + 0];
322             ctrlPointY = currentY + val[k + 1];
323             currentX += val[k + 2];
324             currentY += val[k + 3];
325             break;
326           case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
327             reflectiveCtrlPointX = currentX;
328             reflectiveCtrlPointY = currentY;
329             if (previousCmd == 'c'
330                 || previousCmd == 's'
331                 || previousCmd == 'C'
332                 || previousCmd == 'S') {
333               reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
334               reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
335             }
336             path.cubicTo(
337                 reflectiveCtrlPointX,
338                 reflectiveCtrlPointY,
339                 val[k + 0],
340                 val[k + 1],
341                 val[k + 2],
342                 val[k + 3]);
343             ctrlPointX = val[k + 0];
344             ctrlPointY = val[k + 1];
345             currentX = val[k + 2];
346             currentY = val[k + 3];
347             break;
348           case 'q': // Draws a quadratic Bézier (relative)
349             path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
350             ctrlPointX = currentX + val[k + 0];
351             ctrlPointY = currentY + val[k + 1];
352             currentX += val[k + 2];
353             currentY += val[k + 3];
354             break;
355           case 'Q': // Draws a quadratic Bézier
356             path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
357             ctrlPointX = val[k + 0];
358             ctrlPointY = val[k + 1];
359             currentX = val[k + 2];
360             currentY = val[k + 3];
361             break;
362           case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
363             reflectiveCtrlPointX = 0;
364             reflectiveCtrlPointY = 0;
365             if (previousCmd == 'q'
366                 || previousCmd == 't'
367                 || previousCmd == 'Q'
368                 || previousCmd == 'T') {
369               reflectiveCtrlPointX = currentX - ctrlPointX;
370               reflectiveCtrlPointY = currentY - ctrlPointY;
371             }
372             path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k + 0], val[k + 1]);
373             ctrlPointX = currentX + reflectiveCtrlPointX;
374             ctrlPointY = currentY + reflectiveCtrlPointY;
375             currentX += val[k + 0];
376             currentY += val[k + 1];
377             break;
378           case 'T': // Draws a quadratic Bézier curve (reflective control point)
379             reflectiveCtrlPointX = currentX;
380             reflectiveCtrlPointY = currentY;
381             if (previousCmd == 'q'
382                 || previousCmd == 't'
383                 || previousCmd == 'Q'
384                 || previousCmd == 'T') {
385               reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
386               reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
387             }
388             path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k + 0], val[k + 1]);
389             ctrlPointX = reflectiveCtrlPointX;
390             ctrlPointY = reflectiveCtrlPointY;
391             currentX = val[k + 0];
392             currentY = val[k + 1];
393             break;
394           case 'a': // Draws an elliptical arc
395             // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
396             drawArc(
397                 path,
398                 currentX,
399                 currentY,
400                 val[k + 5] + currentX,
401                 val[k + 6] + currentY,
402                 val[k + 0],
403                 val[k + 1],
404                 val[k + 2],
405                 val[k + 3] != 0,
406                 val[k + 4] != 0);
407             currentX += val[k + 5];
408             currentY += val[k + 6];
409             ctrlPointX = currentX;
410             ctrlPointY = currentY;
411             break;
412           case 'A': // Draws an elliptical arc
413             drawArc(
414                 path,
415                 currentX,
416                 currentY,
417                 val[k + 5],
418                 val[k + 6],
419                 val[k + 0],
420                 val[k + 1],
421                 val[k + 2],
422                 val[k + 3] != 0,
423                 val[k + 4] != 0);
424             currentX = val[k + 5];
425             currentY = val[k + 6];
426             ctrlPointX = currentX;
427             ctrlPointY = currentY;
428             break;
429         }
430         previousCmd = cmd;
431       }
432       current[0] = currentX;
433       current[1] = currentY;
434       current[2] = ctrlPointX;
435       current[3] = ctrlPointY;
436     }
437 
drawArc( Path p, float x0, float y0, float x1, float y1, float a, float b, float theta, boolean isMoreThanHalf, boolean isPositiveArc)438     private static void drawArc(
439         Path p,
440         float x0,
441         float y0,
442         float x1,
443         float y1,
444         float a,
445         float b,
446         float theta,
447         boolean isMoreThanHalf,
448         boolean isPositiveArc) {
449 
450       /* Convert rotation angle from degrees to radians */
451       double thetaD = Math.toRadians(theta);
452       /* Pre-compute rotation matrix entries */
453       double cosTheta = Math.cos(thetaD);
454       double sinTheta = Math.sin(thetaD);
455       /* Transform (x0, y0) and (x1, y1) into unit space */
456       /* using (inverse) rotation, followed by (inverse) scale */
457       double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
458       double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
459       double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
460       double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
461 
462       /* Compute differences and averages */
463       double dx = x0p - x1p;
464       double dy = y0p - y1p;
465       double xm = (x0p + x1p) / 2;
466       double ym = (y0p + y1p) / 2;
467       /* Solve for intersecting unit circles */
468       double dsq = dx * dx + dy * dy;
469       if (dsq == 0.0) {
470         Log.w(LOGTAG, " Points are coincident");
471         return; /* Points are coincident */
472       }
473       double disc = 1.0 / dsq - 1.0 / 4.0;
474       if (disc < 0.0) {
475         Log.w(LOGTAG, "Points are too far apart " + dsq);
476         float adjust = (float) (Math.sqrt(dsq) / 1.99999);
477         drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta, isMoreThanHalf, isPositiveArc);
478         return; /* Points are too far apart */
479       }
480       double s = Math.sqrt(disc);
481       double sdx = s * dx;
482       double sdy = s * dy;
483       double cx;
484       double cy;
485       if (isMoreThanHalf == isPositiveArc) {
486         cx = xm - sdy;
487         cy = ym + sdx;
488       } else {
489         cx = xm + sdy;
490         cy = ym - sdx;
491       }
492 
493       double eta0 = Math.atan2((y0p - cy), (x0p - cx));
494 
495       double eta1 = Math.atan2((y1p - cy), (x1p - cx));
496 
497       double sweep = (eta1 - eta0);
498       if (isPositiveArc != (sweep >= 0)) {
499         if (sweep > 0) {
500           sweep -= 2 * Math.PI;
501         } else {
502           sweep += 2 * Math.PI;
503         }
504       }
505 
506       cx *= a;
507       cy *= b;
508       double tcx = cx;
509       cx = cx * cosTheta - cy * sinTheta;
510       cy = tcx * sinTheta + cy * cosTheta;
511 
512       arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
513     }
514 
515     /**
516      * Converts an arc to cubic Bezier segments and records them in p.
517      *
518      * @param p The target for the cubic Bezier segments
519      * @param cx The x coordinate center of the ellipse
520      * @param cy The y coordinate center of the ellipse
521      * @param a The radius of the ellipse in the horizontal direction
522      * @param b The radius of the ellipse in the vertical direction
523      * @param e1x E(eta1) x coordinate of the starting point of the arc
524      * @param e1y E(eta2) y coordinate of the starting point of the arc
525      * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
526      * @param start The start angle of the arc on the ellipse
527      * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
528      */
arcToBezier( Path p, double cx, double cy, double a, double b, double e1x, double e1y, double theta, double start, double sweep)529     private static void arcToBezier(
530         Path p,
531         double cx,
532         double cy,
533         double a,
534         double b,
535         double e1x,
536         double e1y,
537         double theta,
538         double start,
539         double sweep) {
540       // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
541       // and http://www.spaceroots.org/documents/ellipse/node22.html
542 
543       // Maximum of 45 degrees per cubic Bezier segment
544       int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI));
545 
546       double eta1 = start;
547       double cosTheta = Math.cos(theta);
548       double sinTheta = Math.sin(theta);
549       double cosEta1 = Math.cos(eta1);
550       double sinEta1 = Math.sin(eta1);
551       double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
552       double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
553 
554       double anglePerSegment = sweep / numSegments;
555       for (int i = 0; i < numSegments; i++) {
556         double eta2 = eta1 + anglePerSegment;
557         double sinEta2 = Math.sin(eta2);
558         double cosEta2 = Math.cos(eta2);
559         double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
560         double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
561         double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
562         double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
563         double tanDiff2 = Math.tan((eta2 - eta1) / 2);
564         double alpha = Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
565         double q1x = e1x + alpha * ep1x;
566         double q1y = e1y + alpha * ep1y;
567         double q2x = e2x - alpha * ep2x;
568         double q2y = e2y - alpha * ep2y;
569 
570         p.cubicTo((float) q1x, (float) q1y, (float) q2x, (float) q2y, (float) e2x, (float) e2y);
571         eta1 = eta2;
572         e1x = e2x;
573         e1y = e2y;
574         ep1x = ep2x;
575         ep1y = ep2y;
576       }
577     }
578   }
579 
580   @Implements(value = PathParser.PathData.class, minSdk = N, isInAndroidSdk = false)
581   public static class ShadowPathData {
582     long mNativePathData = 0;
583 
584     @Implementation
__constructor__()585     public void __constructor__() {
586       // mNativePathData = nCreateEmptyPathData();
587     }
588 
589     @Implementation
__constructor__(PathParser.PathData data)590     public void __constructor__(PathParser.PathData data) {
591       // mNativePathData = nCreatePathData(data.mNativePathData);
592     }
593 
594     @Implementation
__constructor__(String pathString)595     public void __constructor__(String pathString) {
596       // mNativePathData = nCreatePathDataFromString(pathString, pathString.length());
597       // if (mNativePathData == 0) {
598       //     throw new IllegalArgumentException("Invalid pathData: " + pathString);
599       // }
600     }
601 
602     @Implementation
getNativePtr()603     public long getNativePtr() {
604       return mNativePathData;
605     }
606 
607     /**
608      * Update the path data to match the source. Before calling this, make sure canMorph(target,
609      * source) is true.
610      *
611      * @param source The source path represented in PathData
612      */
613     @Implementation
setPathData(PathParser.PathData source)614     public void setPathData(PathParser.PathData source) {
615       // nSetPathData(mNativePathData, source.mNativePathData);
616     }
617 
618     @Override
619     @Implementation
finalize()620     protected void finalize() throws Throwable {
621       if (mNativePathData != 0) {
622         //   nFinalize(mNativePathData);
623         mNativePathData = 0;
624       }
625       super.finalize();
626     }
627   }
628 }
629