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