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