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