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