1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.core.graphics;
18 
19 
20 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
21 
22 import android.graphics.Path;
23 import android.util.Log;
24 
25 import androidx.annotation.RestrictTo;
26 
27 import org.jspecify.annotations.NonNull;
28 import org.jspecify.annotations.Nullable;
29 
30 import java.util.ArrayList;
31 
32 // This class is a duplicate from the PathParser.java of frameworks/base, with slight
33 // update on incompatible API like copyOfRange().
34 /**
35  * Parses SVG path strings.
36  */
37 public final class PathParser {
38     private static final String LOGTAG = "PathParser";
39 
40     // Copy from Arrays.copyOfRange() which is only available from API level 9.
41 
42     /**
43      * Copies elements from {@code original} into a new array, from indexes start (inclusive) to
44      * end (exclusive). The original order of elements is preserved.
45      * If {@code end} is greater than {@code original.length}, the result is padded
46      * with the value {@code 0.0f}.
47      *
48      * @param original the original array
49      * @param start    the start index, inclusive
50      * @param end      the end index, exclusive
51      * @return the new array
52      * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length}
53      * @throws IllegalArgumentException       if {@code start > end}
54      * @throws NullPointerException           if {@code original == null}
55      */
copyOfRange(float[] original, int start, int end)56     static float[] copyOfRange(float[] original, int start, int end) {
57         if (start > end) {
58             throw new IllegalArgumentException();
59         }
60         int originalLength = original.length;
61         if (start < 0 || start > originalLength) {
62             throw new ArrayIndexOutOfBoundsException();
63         }
64         int resultLength = end - start;
65         int copyLength = Math.min(resultLength, originalLength - start);
66         float[] result = new float[resultLength];
67         System.arraycopy(original, start, result, 0, copyLength);
68         return result;
69     }
70 
71     /**
72      * Takes a string representation of an SVG path and converts it to a {@link Path}.
73      *
74      * @param pathData The string representing a path, the same as "d" string in svg file.
75      * @return the generated Path object.
76      */
createPathFromPathData(@onNull String pathData)77     public static @NonNull Path createPathFromPathData(@NonNull String pathData) {
78         Path path = new Path();
79         PathDataNode[] nodes = createNodesFromPathData(pathData);
80         try {
81             PathDataNode.nodesToPath(nodes, path);
82         } catch (RuntimeException e) {
83             throw new RuntimeException("Error in parsing " + pathData, e);
84         }
85         return path;
86     }
87 
88     /**
89      * @param pathData The string representing a path, the same as "d" string in svg file.
90      * @return an array of the PathDataNode.
91      */
92     @SuppressWarnings("ArrayReturn")
createNodesFromPathData(@onNull String pathData)93     public static PathDataNode @NonNull [] createNodesFromPathData(@NonNull String pathData) {
94         int start = 0;
95         int end = 1;
96 
97         ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
98         while (end < pathData.length()) {
99             end = nextStart(pathData, end);
100             String s = pathData.substring(start, end).trim();
101             if (!s.isEmpty()) {
102                 float[] val = getFloats(s);
103                 addNode(list, s.charAt(0), val);
104             }
105 
106             start = end;
107             end++;
108         }
109         if ((end - start) == 1 && start < pathData.length()) {
110             addNode(list, pathData.charAt(start), new float[0]);
111         }
112         return list.toArray(new PathDataNode[0]);
113     }
114 
115     /**
116      * @param source The array of PathDataNode to be duplicated.
117      * @return a deep copy of the <code>source</code>.
118      */
119     @SuppressWarnings("ArrayReturn")
deepCopyNodes( @uppressWarnings"ArrayReturn") PathDataNode @onNull [] source )120     public static PathDataNode @NonNull [] deepCopyNodes(
121             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] source
122     ) {
123         PathDataNode[] copy = new PathParser.PathDataNode[source.length];
124         for (int i = 0; i < source.length; i++) {
125             copy[i] = new PathDataNode(source[i]);
126         }
127         return copy;
128     }
129 
130     /**
131      * @param nodesFrom The source path represented in an array of PathDataNode
132      * @param nodesTo   The target path represented in an array of PathDataNode
133      * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
134      */
135     @SuppressWarnings("ArrayReturn")
canMorph( @uppressWarnings"ArrayReturn") PathDataNode @ullable [] nodesFrom, @SuppressWarnings("ArrayReturn") PathDataNode @Nullable [] nodesTo )136     public static boolean canMorph(
137             @SuppressWarnings("ArrayReturn") PathDataNode @Nullable [] nodesFrom,
138             @SuppressWarnings("ArrayReturn") PathDataNode @Nullable [] nodesTo
139     ) {
140         if (nodesFrom == null || nodesTo == null) {
141             return false;
142         }
143 
144         if (nodesFrom.length != nodesTo.length) {
145             return false;
146         }
147 
148         for (int i = 0; i < nodesFrom.length; i++) {
149             if (nodesFrom[i].mType != nodesTo[i].mType
150                     || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) {
151                 return false;
152             }
153         }
154         return true;
155     }
156 
157     /**
158      * Update the target's data to match the source.
159      * Before calling this, make sure canMorph(target, source) is true.
160      *
161      * @param target The target path represented in an array of PathDataNode
162      * @param source The source path represented in an array of PathDataNode
163      */
updateNodes( @uppressWarnings"ArrayReturn") PathDataNode @onNull [] target, @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] source )164     public static void updateNodes(
165             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] target,
166             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] source
167     ) {
168         for (int i = 0; i < source.length; i++) {
169             target[i].mType = source[i].mType;
170             for (int j = 0; j < source[i].mParams.length; j++) {
171                 target[i].mParams[j] = source[i].mParams[j];
172             }
173         }
174     }
175 
nextStart(String s, int end)176     private static int nextStart(String s, int end) {
177         char c;
178 
179         while (end < s.length()) {
180             c = s.charAt(end);
181             // Note that 'e' or 'E' are not valid path commands, but could be
182             // used for floating point numbers' scientific notation.
183             // Therefore, when searching for next command, we should ignore 'e'
184             // and 'E'.
185             if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
186                     && c != 'e' && c != 'E') {
187                 return end;
188             }
189             end++;
190         }
191         return end;
192     }
193 
addNode(ArrayList<PathDataNode> list, char cmd, float[] val)194     private static void addNode(ArrayList<PathDataNode> list, char cmd, float[] val) {
195         list.add(new PathDataNode(cmd, val));
196     }
197 
198     private static class ExtractFloatResult {
199         // We need to return the position of the next separator and whether the
200         // next float starts with a '-' or a '.'.
201         int mEndPosition;
202         boolean mEndWithNegOrDot;
203 
ExtractFloatResult()204         ExtractFloatResult() {
205         }
206     }
207 
208     /**
209      * Parse the floats in the string.
210      * This is an optimized version of parseFloat(s.split(",|\\s"));
211      *
212      * @param s the string containing a command and list of floats
213      * @return array of floats
214      */
getFloats(String s)215     private static float[] getFloats(String s) {
216         if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
217             return new float[0];
218         }
219         try {
220             float[] results = new float[s.length()];
221             int count = 0;
222             int startPosition = 1;
223             int endPosition = 0;
224 
225             ExtractFloatResult result = new ExtractFloatResult();
226             int totalLength = s.length();
227 
228             // The startPosition should always be the first character of the
229             // current number, and endPosition is the character after the current
230             // number.
231             while (startPosition < totalLength) {
232                 extract(s, startPosition, result);
233                 endPosition = result.mEndPosition;
234 
235                 if (startPosition < endPosition) {
236                     results[count++] = Float.parseFloat(
237                             s.substring(startPosition, endPosition));
238                 }
239 
240                 if (result.mEndWithNegOrDot) {
241                     // Keep the '-' or '.' sign with next number.
242                     startPosition = endPosition;
243                 } else {
244                     startPosition = endPosition + 1;
245                 }
246             }
247             return copyOfRange(results, 0, count);
248         } catch (NumberFormatException e) {
249             throw new RuntimeException("error in parsing \"" + s + "\"", e);
250         }
251     }
252 
253     /**
254      * Calculate the position of the next comma or space or negative sign
255      *
256      * @param s      the string to search
257      * @param start  the position to start searching
258      * @param result the result of the extraction, including the position of the
259      *               the starting position of next number, whether it is ending with a '-'.
260      */
extract(String s, int start, ExtractFloatResult result)261     private static void extract(String s, int start, ExtractFloatResult result) {
262         // Now looking for ' ', ',', '.' or '-' from the start.
263         int currentIndex = start;
264         boolean foundSeparator = false;
265         result.mEndWithNegOrDot = false;
266         boolean secondDot = false;
267         boolean isExponential = false;
268         for (; currentIndex < s.length(); currentIndex++) {
269             boolean isPrevExponential = isExponential;
270             isExponential = false;
271             char currentChar = s.charAt(currentIndex);
272             switch (currentChar) {
273                 case ' ':
274                 case ',':
275                     foundSeparator = true;
276                     break;
277                 case '-':
278                     // The negative sign following a 'e' or 'E' is not a separator.
279                     if (currentIndex != start && !isPrevExponential) {
280                         foundSeparator = true;
281                         result.mEndWithNegOrDot = true;
282                     }
283                     break;
284                 case '.':
285                     if (!secondDot) {
286                         secondDot = true;
287                     } else {
288                         // This is the second dot, and it is considered as a separator.
289                         foundSeparator = true;
290                         result.mEndWithNegOrDot = true;
291                     }
292                     break;
293                 case 'e':
294                 case 'E':
295                     isExponential = true;
296                     break;
297             }
298             if (foundSeparator) {
299                 break;
300             }
301         }
302         // When there is nothing found, then we put the end position to the end
303         // of the string.
304         result.mEndPosition = currentIndex;
305     }
306 
307     /**
308      * Interpolate between two arrays of PathDataNodes with the given fraction, and store the
309      * results in the first parameter.
310      *
311      * @param target   The resulting array of {@link PathDataNode} for the interpolation
312      * @param fraction A float fraction value in the range of 0 to 1
313      * @param from     The array of {@link PathDataNode} when fraction is 0
314      * @param to       The array of {@link PathDataNode} when the fraction is 1
315      * @throws IllegalArgumentException When the arrays of nodes are incompatible for interpolation.
316      * @see #canMorph(PathDataNode[], PathDataNode[])
317      */
interpolatePathDataNodes( @uppressWarnings"ArrayReturn") PathDataNode @onNull [] target, float fraction, @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] from, @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] to )318     public static void interpolatePathDataNodes(
319             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] target,
320             float fraction,
321             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] from,
322             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] to
323     ) {
324         if (!interpolatePathDataNodes(target, from, to, fraction)) {
325             throw new IllegalArgumentException(
326                     "Can't interpolate between two incompatible pathData"
327             );
328         }
329     }
330 
331     /**
332      * Interpolate between two arrays of PathDataNodes with the given fraction, and store the
333      * results in the first parameter.
334      *
335      * @param target   The resulting array of {@link PathDataNode} for the interpolation
336      * @param from     The array of {@link PathDataNode} when fraction is 0
337      * @param to       The array of {@link PathDataNode} when the fraction is 1
338      * @param fraction A float fraction value in the range of 0 to 1
339      * @throws IllegalArgumentException When the arrays of nodes are incompatible for interpolation.
340      * @see #canMorph(PathDataNode[], PathDataNode[])
341      * @deprecated Use
342      * {@link #interpolatePathDataNodes(PathDataNode[], float, PathDataNode[], PathDataNode[])}
343      * instead.
344      */
345     @Deprecated
346     @RestrictTo(LIBRARY_GROUP_PREFIX)
interpolatePathDataNodes( @uppressWarnings"ArrayReturn") PathDataNode @onNull [] target, @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] from, @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] to, float fraction )347     public static boolean interpolatePathDataNodes(
348             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] target,
349             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] from,
350             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] to,
351             float fraction
352     ) {
353         if (target.length != from.length || from.length != to.length) {
354             throw new IllegalArgumentException("The nodes to be interpolated and resulting nodes"
355                     + " must have the same length");
356         }
357 
358         if (!canMorph(from, to)) {
359             return false;
360         }
361         // Now do the interpolation
362         for (int i = 0; i < target.length; i++) {
363             target[i].interpolatePathDataNode(from[i], to[i], fraction);
364         }
365         return true;
366     }
367 
368     /**
369      * Convert an array of PathDataNode to Path.
370      *
371      * @param node The source array of PathDataNode.
372      * @param path The target Path object.
373      */
374     @SuppressWarnings("ArrayReturn")
nodesToPath( @uppressWarnings"ArrayReturn") PathDataNode @onNull [] node, @NonNull Path path )375     public static void nodesToPath(
376             @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] node,
377             @NonNull Path path
378     ) {
379         float[] current = new float[6];
380         char previousCommand = 'm';
381         for (PathDataNode pathDataNode : node) {
382             PathDataNode.addCommand(path, current, previousCommand, pathDataNode.mType,
383                     pathDataNode.mParams);
384             previousCommand = pathDataNode.mType;
385         }
386     }
387 
388     /**
389      * Each PathDataNode represents one command in the "d" attribute of the svg
390      * file.
391      * An array of PathDataNode can represent the whole "d" attribute.
392      */
393     public static class PathDataNode {
394 
395         /**
396          */
397         private char mType;
398 
399         /**
400          */
401         private final float[] mParams;
402 
getType()403         public char getType() {
404             return mType;
405         }
406 
getParams()407         public float @NonNull [] getParams() {
408             return mParams;
409         }
410 
PathDataNode(char type, float[] params)411         PathDataNode(char type, float[] params) {
412             this.mType = type;
413             this.mParams = params;
414         }
415 
PathDataNode(PathDataNode n)416         PathDataNode(PathDataNode n) {
417             mType = n.mType;
418             mParams = copyOfRange(n.mParams, 0, n.mParams.length);
419         }
420 
421         /**
422          * Convert an array of PathDataNode to Path.
423          *
424          * @param node The source array of PathDataNode.
425          * @param path The target Path object.
426          * @deprecated Use {@link PathParser#nodesToPath(PathDataNode[], Path)} instead.
427          */
428         @Deprecated
429         @RestrictTo(LIBRARY_GROUP_PREFIX)
430         @SuppressWarnings("ArrayReturn")
nodesToPath( @uppressWarnings"ArrayReturn") PathDataNode @onNull [] node, @NonNull Path path )431         public static void nodesToPath(
432                 @SuppressWarnings("ArrayReturn") PathDataNode @NonNull [] node,
433                 @NonNull Path path
434         ) {
435             PathParser.nodesToPath(node, path);
436         }
437 
438         /**
439          * The current PathDataNode will be interpolated between the
440          * <code>nodeFrom</code> and <code>nodeTo</code> according to the
441          * <code>fraction</code>.
442          *
443          * @param nodeFrom The start value as a PathDataNode.
444          * @param nodeTo   The end value as a PathDataNode
445          * @param fraction The fraction to interpolate.
446          */
interpolatePathDataNode(@onNull PathDataNode nodeFrom, @NonNull PathDataNode nodeTo, float fraction)447         public void interpolatePathDataNode(@NonNull PathDataNode nodeFrom,
448                 @NonNull PathDataNode nodeTo, float fraction) {
449             mType = nodeFrom.mType;
450             for (int i = 0; i < nodeFrom.mParams.length; i++) {
451                 mParams[i] = nodeFrom.mParams[i] * (1 - fraction)
452                         + nodeTo.mParams[i] * fraction;
453             }
454         }
455 
addCommand(Path path, float[] current, char previousCmd, char cmd, float[] val)456         private static void addCommand(Path path, float[] current,
457                 char previousCmd, char cmd, float[] val) {
458 
459             int incr = 2;
460             float currentX = current[0];
461             float currentY = current[1];
462             float ctrlPointX = current[2];
463             float ctrlPointY = current[3];
464             float currentSegmentStartX = current[4];
465             float currentSegmentStartY = current[5];
466             float reflectiveCtrlPointX;
467             float reflectiveCtrlPointY;
468 
469             switch (cmd) {
470                 case 'z':
471                 case 'Z':
472                     path.close();
473                     // Path is closed here, but we need to move the pen to the
474                     // closed position. So we cache the segment's starting position,
475                     // and restore it here.
476                     currentX = currentSegmentStartX;
477                     currentY = currentSegmentStartY;
478                     ctrlPointX = currentSegmentStartX;
479                     ctrlPointY = currentSegmentStartY;
480                     path.moveTo(currentX, currentY);
481                     break;
482                 case 'm':
483                 case 'M':
484                 case 'l':
485                 case 'L':
486                 case 't':
487                 case 'T':
488                     incr = 2;
489                     break;
490                 case 'h':
491                 case 'H':
492                 case 'v':
493                 case 'V':
494                     incr = 1;
495                     break;
496                 case 'c':
497                 case 'C':
498                     incr = 6;
499                     break;
500                 case 's':
501                 case 'S':
502                 case 'q':
503                 case 'Q':
504                     incr = 4;
505                     break;
506                 case 'a':
507                 case 'A':
508                     incr = 7;
509                     break;
510             }
511 
512             for (int k = 0; k < val.length; k += incr) {
513                 switch (cmd) {
514                     case 'm': // moveto - Start a new sub-path (relative)
515                         currentX += val[k + 0];
516                         currentY += val[k + 1];
517                         if (k > 0) {
518                             // According to the spec, if a moveto is followed by multiple
519                             // pairs of coordinates, the subsequent pairs are treated as
520                             // implicit lineto commands.
521                             path.rLineTo(val[k + 0], val[k + 1]);
522                         } else {
523                             path.rMoveTo(val[k + 0], val[k + 1]);
524                             currentSegmentStartX = currentX;
525                             currentSegmentStartY = currentY;
526                         }
527                         break;
528                     case 'M': // moveto - Start a new sub-path
529                         currentX = val[k + 0];
530                         currentY = val[k + 1];
531                         if (k > 0) {
532                             // According to the spec, if a moveto is followed by multiple
533                             // pairs of coordinates, the subsequent pairs are treated as
534                             // implicit lineto commands.
535                             path.lineTo(val[k + 0], val[k + 1]);
536                         } else {
537                             path.moveTo(val[k + 0], val[k + 1]);
538                             currentSegmentStartX = currentX;
539                             currentSegmentStartY = currentY;
540                         }
541                         break;
542                     case 'l': // lineto - Draw a line from the current point (relative)
543                         path.rLineTo(val[k + 0], val[k + 1]);
544                         currentX += val[k + 0];
545                         currentY += val[k + 1];
546                         break;
547                     case 'L': // lineto - Draw a line from the current point
548                         path.lineTo(val[k + 0], val[k + 1]);
549                         currentX = val[k + 0];
550                         currentY = val[k + 1];
551                         break;
552                     case 'h': // horizontal lineto - Draws a horizontal line (relative)
553                         path.rLineTo(val[k + 0], 0);
554                         currentX += val[k + 0];
555                         break;
556                     case 'H': // horizontal lineto - Draws a horizontal line
557                         path.lineTo(val[k + 0], currentY);
558                         currentX = val[k + 0];
559                         break;
560                     case 'v': // vertical lineto - Draws a vertical line from the current point (r)
561                         path.rLineTo(0, val[k + 0]);
562                         currentY += val[k + 0];
563                         break;
564                     case 'V': // vertical lineto - Draws a vertical line from the current point
565                         path.lineTo(currentX, val[k + 0]);
566                         currentY = val[k + 0];
567                         break;
568                     case 'c': // curveto - Draws a cubic Bézier curve (relative)
569                         path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
570                                 val[k + 4], val[k + 5]);
571 
572                         ctrlPointX = currentX + val[k + 2];
573                         ctrlPointY = currentY + val[k + 3];
574                         currentX += val[k + 4];
575                         currentY += val[k + 5];
576 
577                         break;
578                     case 'C': // curveto - Draws a cubic Bézier curve
579                         path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
580                                 val[k + 4], val[k + 5]);
581                         currentX = val[k + 4];
582                         currentY = val[k + 5];
583                         ctrlPointX = val[k + 2];
584                         ctrlPointY = val[k + 3];
585                         break;
586                     case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
587                         reflectiveCtrlPointX = 0;
588                         reflectiveCtrlPointY = 0;
589                         if (previousCmd == 'c' || previousCmd == 's'
590                                 || previousCmd == 'C' || previousCmd == 'S') {
591                             reflectiveCtrlPointX = currentX - ctrlPointX;
592                             reflectiveCtrlPointY = currentY - ctrlPointY;
593                         }
594                         path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
595                                 val[k + 0], val[k + 1],
596                                 val[k + 2], val[k + 3]);
597 
598                         ctrlPointX = currentX + val[k + 0];
599                         ctrlPointY = currentY + val[k + 1];
600                         currentX += val[k + 2];
601                         currentY += val[k + 3];
602                         break;
603                     case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
604                         reflectiveCtrlPointX = currentX;
605                         reflectiveCtrlPointY = currentY;
606                         if (previousCmd == 'c' || previousCmd == 's'
607                                 || previousCmd == 'C' || previousCmd == 'S') {
608                             reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
609                             reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
610                         }
611                         path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
612                                 val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
613                         ctrlPointX = val[k + 0];
614                         ctrlPointY = val[k + 1];
615                         currentX = val[k + 2];
616                         currentY = val[k + 3];
617                         break;
618                     case 'q': // Draws a quadratic Bézier (relative)
619                         path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
620                         ctrlPointX = currentX + val[k + 0];
621                         ctrlPointY = currentY + val[k + 1];
622                         currentX += val[k + 2];
623                         currentY += val[k + 3];
624                         break;
625                     case 'Q': // Draws a quadratic Bézier
626                         path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
627                         ctrlPointX = val[k + 0];
628                         ctrlPointY = val[k + 1];
629                         currentX = val[k + 2];
630                         currentY = val[k + 3];
631                         break;
632                     case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
633                         reflectiveCtrlPointX = 0;
634                         reflectiveCtrlPointY = 0;
635                         if (previousCmd == 'q' || previousCmd == 't'
636                                 || previousCmd == 'Q' || previousCmd == 'T') {
637                             reflectiveCtrlPointX = currentX - ctrlPointX;
638                             reflectiveCtrlPointY = currentY - ctrlPointY;
639                         }
640                         path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
641                                 val[k + 0], val[k + 1]);
642                         ctrlPointX = currentX + reflectiveCtrlPointX;
643                         ctrlPointY = currentY + reflectiveCtrlPointY;
644                         currentX += val[k + 0];
645                         currentY += val[k + 1];
646                         break;
647                     case 'T': // Draws a quadratic Bézier curve (reflective control point)
648                         reflectiveCtrlPointX = currentX;
649                         reflectiveCtrlPointY = currentY;
650                         if (previousCmd == 'q' || previousCmd == 't'
651                                 || previousCmd == 'Q' || previousCmd == 'T') {
652                             reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
653                             reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
654                         }
655                         path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
656                                 val[k + 0], val[k + 1]);
657                         ctrlPointX = reflectiveCtrlPointX;
658                         ctrlPointY = reflectiveCtrlPointY;
659                         currentX = val[k + 0];
660                         currentY = val[k + 1];
661                         break;
662                     case 'a': // Draws an elliptical arc
663                         // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
664                         drawArc(path,
665                                 currentX,
666                                 currentY,
667                                 val[k + 5] + currentX,
668                                 val[k + 6] + currentY,
669                                 val[k + 0],
670                                 val[k + 1],
671                                 val[k + 2],
672                                 val[k + 3] != 0,
673                                 val[k + 4] != 0);
674                         currentX += val[k + 5];
675                         currentY += val[k + 6];
676                         ctrlPointX = currentX;
677                         ctrlPointY = currentY;
678                         break;
679                     case 'A': // Draws an elliptical arc
680                         drawArc(path,
681                                 currentX,
682                                 currentY,
683                                 val[k + 5],
684                                 val[k + 6],
685                                 val[k + 0],
686                                 val[k + 1],
687                                 val[k + 2],
688                                 val[k + 3] != 0,
689                                 val[k + 4] != 0);
690                         currentX = val[k + 5];
691                         currentY = val[k + 6];
692                         ctrlPointX = currentX;
693                         ctrlPointY = currentY;
694                         break;
695                 }
696                 previousCmd = cmd;
697             }
698             current[0] = currentX;
699             current[1] = currentY;
700             current[2] = ctrlPointX;
701             current[3] = ctrlPointY;
702             current[4] = currentSegmentStartX;
703             current[5] = currentSegmentStartY;
704         }
705 
drawArc(Path p, float x0, float y0, float x1, float y1, float a, float b, float theta, boolean isMoreThanHalf, boolean isPositiveArc)706         private static void drawArc(Path p,
707                 float x0,
708                 float y0,
709                 float x1,
710                 float y1,
711                 float a,
712                 float b,
713                 float theta,
714                 boolean isMoreThanHalf,
715                 boolean isPositiveArc) {
716 
717             /* Convert rotation angle from degrees to radians */
718             double thetaD = Math.toRadians(theta);
719             /* Pre-compute rotation matrix entries */
720             double cosTheta = Math.cos(thetaD);
721             double sinTheta = Math.sin(thetaD);
722             /* Transform (x0, y0) and (x1, y1) into unit space */
723             /* using (inverse) rotation, followed by (inverse) scale */
724             double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
725             double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
726             double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
727             double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
728 
729             /* Compute differences and averages */
730             double dx = x0p - x1p;
731             double dy = y0p - y1p;
732             double xm = (x0p + x1p) / 2;
733             double ym = (y0p + y1p) / 2;
734             /* Solve for intersecting unit circles */
735             double dsq = dx * dx + dy * dy;
736             if (dsq == 0.0) {
737                 Log.w(LOGTAG, " Points are coincident");
738                 return; /* Points are coincident */
739             }
740             double disc = 1.0 / dsq - 1.0 / 4.0;
741             if (disc < 0.0) {
742                 Log.w(LOGTAG, "Points are too far apart " + dsq);
743                 float adjust = (float) (Math.sqrt(dsq) / 1.99999);
744                 drawArc(p, x0, y0, x1, y1, a * adjust,
745                         b * adjust, theta, isMoreThanHalf, isPositiveArc);
746                 return; /* Points are too far apart */
747             }
748             double s = Math.sqrt(disc);
749             double sdx = s * dx;
750             double sdy = s * dy;
751             double cx;
752             double cy;
753             if (isMoreThanHalf == isPositiveArc) {
754                 cx = xm - sdy;
755                 cy = ym + sdx;
756             } else {
757                 cx = xm + sdy;
758                 cy = ym - sdx;
759             }
760 
761             double eta0 = Math.atan2((y0p - cy), (x0p - cx));
762 
763             double eta1 = Math.atan2((y1p - cy), (x1p - cx));
764 
765             double sweep = (eta1 - eta0);
766             if (isPositiveArc != (sweep >= 0)) {
767                 if (sweep > 0) {
768                     sweep -= 2 * Math.PI;
769                 } else {
770                     sweep += 2 * Math.PI;
771                 }
772             }
773 
774             cx *= a;
775             cy *= b;
776             double tcx = cx;
777             cx = cx * cosTheta - cy * sinTheta;
778             cy = tcx * sinTheta + cy * cosTheta;
779 
780             arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
781         }
782 
783         /**
784          * Converts an arc to cubic Bezier segments and records them in p.
785          *
786          * @param p     The target for the cubic Bezier segments
787          * @param cx    The x coordinate center of the ellipse
788          * @param cy    The y coordinate center of the ellipse
789          * @param a     The radius of the ellipse in the horizontal direction
790          * @param b     The radius of the ellipse in the vertical direction
791          * @param e1x   E(eta1) x coordinate of the starting point of the arc
792          * @param e1y   E(eta2) y coordinate of the starting point of the arc
793          * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
794          * @param start The start angle of the arc on the ellipse
795          * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
796          */
arcToBezier(Path p, double cx, double cy, double a, double b, double e1x, double e1y, double theta, double start, double sweep)797         private static void arcToBezier(Path p,
798                 double cx,
799                 double cy,
800                 double a,
801                 double b,
802                 double e1x,
803                 double e1y,
804                 double theta,
805                 double start,
806                 double sweep) {
807             // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
808             // and http://www.spaceroots.org/documents/ellipse/node22.html
809 
810             // Maximum of 45 degrees per cubic Bezier segment
811             int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));
812 
813             double eta1 = start;
814             double cosTheta = Math.cos(theta);
815             double sinTheta = Math.sin(theta);
816             double cosEta1 = Math.cos(eta1);
817             double sinEta1 = Math.sin(eta1);
818             double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
819             double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
820 
821             double anglePerSegment = sweep / numSegments;
822             for (int i = 0; i < numSegments; i++) {
823                 double eta2 = eta1 + anglePerSegment;
824                 double sinEta2 = Math.sin(eta2);
825                 double cosEta2 = Math.cos(eta2);
826                 double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
827                 double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
828                 double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
829                 double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
830                 double tanDiff2 = Math.tan((eta2 - eta1) / 2);
831                 double alpha =
832                         Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
833                 double q1x = e1x + alpha * ep1x;
834                 double q1y = e1y + alpha * ep1y;
835                 double q2x = e2x - alpha * ep2x;
836                 double q2y = e2y - alpha * ep2y;
837 
838                 // Adding this no-op call to workaround a proguard related issue.
839                 p.rLineTo(0, 0);
840 
841                 p.cubicTo((float) q1x,
842                         (float) q1y,
843                         (float) q2x,
844                         (float) q2y,
845                         (float) e2x,
846                         (float) e2y);
847                 eta1 = eta2;
848                 e1x = e2x;
849                 e1y = e2y;
850                 ep1x = ep2x;
851                 ep1y = ep2y;
852             }
853         }
854     }
855 
PathParser()856     private PathParser() {
857     }
858 }
859