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