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