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 165 /** 166 * Convert an array of PathDataNode to Path. 167 * 168 * @param node The source array of PathDataNode. 169 * @param path The target Path object. 170 */ nodesToPath(PathDataNode[] node, Path path)171 public static void nodesToPath(PathDataNode[] node, Path path) { 172 float[] current = new float[4]; 173 char previousCommand = 'm'; 174 for (int i = 0; i < node.length; i++) { 175 addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); 176 previousCommand = node[i].mType; 177 } 178 } 179 180 /** 181 * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and <code> 182 * nodeTo</code> according to the <code>fraction</code>. 183 * 184 * @param nodeFrom The start value as a PathDataNode. 185 * @param nodeTo The end value as a PathDataNode 186 * @param fraction The fraction to interpolate. 187 */ interpolatePathDataNode( PathDataNode nodeFrom, PathDataNode nodeTo, float fraction)188 public void interpolatePathDataNode( 189 PathDataNode nodeFrom, PathDataNode nodeTo, float fraction) { 190 for (int i = 0; i < nodeFrom.mParams.length; i++) { 191 mParams[i] = nodeFrom.mParams[i] * (1 - fraction) + nodeTo.mParams[i] * fraction; 192 } 193 } 194 addCommand( Path path, float[] current, char previousCmd, char cmd, float[] val)195 private static void addCommand( 196 Path path, float[] current, char previousCmd, char cmd, float[] val) { 197 198 int incr = 2; 199 float currentX = current[0]; 200 float currentY = current[1]; 201 float ctrlPointX = current[2]; 202 float ctrlPointY = current[3]; 203 float reflectiveCtrlPointX; 204 float reflectiveCtrlPointY; 205 206 switch (cmd) { 207 case 'z': 208 case 'Z': 209 path.close(); 210 return; 211 case 'm': 212 case 'M': 213 case 'l': 214 case 'L': 215 case 't': 216 case 'T': 217 incr = 2; 218 break; 219 case 'h': 220 case 'H': 221 case 'v': 222 case 'V': 223 incr = 1; 224 break; 225 case 'c': 226 case 'C': 227 incr = 6; 228 break; 229 case 's': 230 case 'S': 231 case 'q': 232 case 'Q': 233 incr = 4; 234 break; 235 case 'a': 236 case 'A': 237 incr = 7; 238 break; 239 } 240 for (int k = 0; k < val.length; k += incr) { 241 switch (cmd) { 242 case 'm': // moveto - Start a new sub-path (relative) 243 path.rMoveTo(val[k + 0], val[k + 1]); 244 currentX += val[k + 0]; 245 currentY += val[k + 1]; 246 break; 247 case 'M': // moveto - Start a new sub-path 248 path.moveTo(val[k + 0], val[k + 1]); 249 currentX = val[k + 0]; 250 currentY = val[k + 1]; 251 break; 252 case 'l': // lineto - Draw a line from the current point (relative) 253 path.rLineTo(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 258 path.lineTo(val[k + 0], val[k + 1]); 259 currentX = val[k + 0]; 260 currentY = val[k + 1]; 261 break; 262 case 'z': // closepath - Close the current subpath 263 case 'Z': // closepath - Close the current subpath 264 path.close(); 265 break; 266 case 'h': // horizontal lineto - Draws a horizontal line (relative) 267 path.rLineTo(val[k + 0], 0); 268 currentX += val[k + 0]; 269 break; 270 case 'H': // horizontal lineto - Draws a horizontal line 271 path.lineTo(val[k + 0], currentY); 272 currentX = val[k + 0]; 273 break; 274 case 'v': // vertical lineto - Draws a vertical line from the current point (r) 275 path.rLineTo(0, val[k + 0]); 276 currentY += val[k + 0]; 277 break; 278 case 'V': // vertical lineto - Draws a vertical line from the current point 279 path.lineTo(currentX, val[k + 0]); 280 currentY = val[k + 0]; 281 break; 282 case 'c': // curveto - Draws a cubic Bézier curve (relative) 283 path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]); 284 285 ctrlPointX = currentX + val[k + 2]; 286 ctrlPointY = currentY + val[k + 3]; 287 currentX += val[k + 4]; 288 currentY += val[k + 5]; 289 290 break; 291 case 'C': // curveto - Draws a cubic Bézier curve 292 path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]); 293 currentX = val[k + 4]; 294 currentY = val[k + 5]; 295 ctrlPointX = val[k + 2]; 296 ctrlPointY = val[k + 3]; 297 break; 298 case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) 299 reflectiveCtrlPointX = 0; 300 reflectiveCtrlPointY = 0; 301 if (previousCmd == 'c' 302 || previousCmd == 's' 303 || previousCmd == 'C' 304 || previousCmd == 'S') { 305 reflectiveCtrlPointX = currentX - ctrlPointX; 306 reflectiveCtrlPointY = currentY - ctrlPointY; 307 } 308 path.rCubicTo( 309 reflectiveCtrlPointX, 310 reflectiveCtrlPointY, 311 val[k + 0], 312 val[k + 1], 313 val[k + 2], 314 val[k + 3]); 315 316 ctrlPointX = currentX + val[k + 0]; 317 ctrlPointY = currentY + val[k + 1]; 318 currentX += val[k + 2]; 319 currentY += val[k + 3]; 320 break; 321 case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) 322 reflectiveCtrlPointX = currentX; 323 reflectiveCtrlPointY = currentY; 324 if (previousCmd == 'c' 325 || previousCmd == 's' 326 || previousCmd == 'C' 327 || previousCmd == 'S') { 328 reflectiveCtrlPointX = 2 * currentX - ctrlPointX; 329 reflectiveCtrlPointY = 2 * currentY - ctrlPointY; 330 } 331 path.cubicTo( 332 reflectiveCtrlPointX, 333 reflectiveCtrlPointY, 334 val[k + 0], 335 val[k + 1], 336 val[k + 2], 337 val[k + 3]); 338 ctrlPointX = val[k + 0]; 339 ctrlPointY = val[k + 1]; 340 currentX = val[k + 2]; 341 currentY = val[k + 3]; 342 break; 343 case 'q': // Draws a quadratic Bézier (relative) 344 path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 345 ctrlPointX = currentX + val[k + 0]; 346 ctrlPointY = currentY + val[k + 1]; 347 currentX += val[k + 2]; 348 currentY += val[k + 3]; 349 break; 350 case 'Q': // Draws a quadratic Bézier 351 path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 352 ctrlPointX = val[k + 0]; 353 ctrlPointY = val[k + 1]; 354 currentX = val[k + 2]; 355 currentY = val[k + 3]; 356 break; 357 case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) 358 reflectiveCtrlPointX = 0; 359 reflectiveCtrlPointY = 0; 360 if (previousCmd == 'q' 361 || previousCmd == 't' 362 || previousCmd == 'Q' 363 || previousCmd == 'T') { 364 reflectiveCtrlPointX = currentX - ctrlPointX; 365 reflectiveCtrlPointY = currentY - ctrlPointY; 366 } 367 path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k + 0], val[k + 1]); 368 ctrlPointX = currentX + reflectiveCtrlPointX; 369 ctrlPointY = currentY + reflectiveCtrlPointY; 370 currentX += val[k + 0]; 371 currentY += val[k + 1]; 372 break; 373 case 'T': // Draws a quadratic Bézier curve (reflective control point) 374 reflectiveCtrlPointX = currentX; 375 reflectiveCtrlPointY = currentY; 376 if (previousCmd == 'q' 377 || previousCmd == 't' 378 || previousCmd == 'Q' 379 || previousCmd == 'T') { 380 reflectiveCtrlPointX = 2 * currentX - ctrlPointX; 381 reflectiveCtrlPointY = 2 * currentY - ctrlPointY; 382 } 383 path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k + 0], val[k + 1]); 384 ctrlPointX = reflectiveCtrlPointX; 385 ctrlPointY = reflectiveCtrlPointY; 386 currentX = val[k + 0]; 387 currentY = val[k + 1]; 388 break; 389 case 'a': // Draws an elliptical arc 390 // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) 391 drawArc( 392 path, 393 currentX, 394 currentY, 395 val[k + 5] + currentX, 396 val[k + 6] + currentY, 397 val[k + 0], 398 val[k + 1], 399 val[k + 2], 400 val[k + 3] != 0, 401 val[k + 4] != 0); 402 currentX += val[k + 5]; 403 currentY += val[k + 6]; 404 ctrlPointX = currentX; 405 ctrlPointY = currentY; 406 break; 407 case 'A': // Draws an elliptical arc 408 drawArc( 409 path, 410 currentX, 411 currentY, 412 val[k + 5], 413 val[k + 6], 414 val[k + 0], 415 val[k + 1], 416 val[k + 2], 417 val[k + 3] != 0, 418 val[k + 4] != 0); 419 currentX = val[k + 5]; 420 currentY = val[k + 6]; 421 ctrlPointX = currentX; 422 ctrlPointY = currentY; 423 break; 424 } 425 previousCmd = cmd; 426 } 427 current[0] = currentX; 428 current[1] = currentY; 429 current[2] = ctrlPointX; 430 current[3] = ctrlPointY; 431 } 432 drawArc( Path p, float x0, float y0, float x1, float y1, float a, float b, float theta, boolean isMoreThanHalf, boolean isPositiveArc)433 private static void drawArc( 434 Path p, 435 float x0, 436 float y0, 437 float x1, 438 float y1, 439 float a, 440 float b, 441 float theta, 442 boolean isMoreThanHalf, 443 boolean isPositiveArc) { 444 445 /* Convert rotation angle from degrees to radians */ 446 double thetaD = Math.toRadians(theta); 447 /* Pre-compute rotation matrix entries */ 448 double cosTheta = Math.cos(thetaD); 449 double sinTheta = Math.sin(thetaD); 450 /* Transform (x0, y0) and (x1, y1) into unit space */ 451 /* using (inverse) rotation, followed by (inverse) scale */ 452 double x0p = (x0 * cosTheta + y0 * sinTheta) / a; 453 double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; 454 double x1p = (x1 * cosTheta + y1 * sinTheta) / a; 455 double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; 456 457 /* Compute differences and averages */ 458 double dx = x0p - x1p; 459 double dy = y0p - y1p; 460 double xm = (x0p + x1p) / 2; 461 double ym = (y0p + y1p) / 2; 462 /* Solve for intersecting unit circles */ 463 double dsq = dx * dx + dy * dy; 464 if (dsq == 0.0) { 465 Log.w(LOGTAG, " Points are coincident"); 466 return; /* Points are coincident */ 467 } 468 double disc = 1.0 / dsq - 1.0 / 4.0; 469 if (disc < 0.0) { 470 Log.w(LOGTAG, "Points are too far apart " + dsq); 471 float adjust = (float) (Math.sqrt(dsq) / 1.99999); 472 drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta, isMoreThanHalf, isPositiveArc); 473 return; /* Points are too far apart */ 474 } 475 double s = Math.sqrt(disc); 476 double sdx = s * dx; 477 double sdy = s * dy; 478 double cx; 479 double cy; 480 if (isMoreThanHalf == isPositiveArc) { 481 cx = xm - sdy; 482 cy = ym + sdx; 483 } else { 484 cx = xm + sdy; 485 cy = ym - sdx; 486 } 487 488 double eta0 = Math.atan2((y0p - cy), (x0p - cx)); 489 490 double eta1 = Math.atan2((y1p - cy), (x1p - cx)); 491 492 double sweep = (eta1 - eta0); 493 if (isPositiveArc != (sweep >= 0)) { 494 if (sweep > 0) { 495 sweep -= 2 * Math.PI; 496 } else { 497 sweep += 2 * Math.PI; 498 } 499 } 500 501 cx *= a; 502 cy *= b; 503 double tcx = cx; 504 cx = cx * cosTheta - cy * sinTheta; 505 cy = tcx * sinTheta + cy * cosTheta; 506 507 arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); 508 } 509 510 /** 511 * Converts an arc to cubic Bezier segments and records them in p. 512 * 513 * @param p The target for the cubic Bezier segments 514 * @param cx The x coordinate center of the ellipse 515 * @param cy The y coordinate center of the ellipse 516 * @param a The radius of the ellipse in the horizontal direction 517 * @param b The radius of the ellipse in the vertical direction 518 * @param e1x E(eta1) x coordinate of the starting point of the arc 519 * @param e1y E(eta2) y coordinate of the starting point of the arc 520 * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane 521 * @param start The start angle of the arc on the ellipse 522 * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse 523 */ arcToBezier( Path p, double cx, double cy, double a, double b, double e1x, double e1y, double theta, double start, double sweep)524 private static void arcToBezier( 525 Path p, 526 double cx, 527 double cy, 528 double a, 529 double b, 530 double e1x, 531 double e1y, 532 double theta, 533 double start, 534 double sweep) { 535 // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html 536 // and http://www.spaceroots.org/documents/ellipse/node22.html 537 538 // Maximum of 45 degrees per cubic Bezier segment 539 int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); 540 541 double eta1 = start; 542 double cosTheta = Math.cos(theta); 543 double sinTheta = Math.sin(theta); 544 double cosEta1 = Math.cos(eta1); 545 double sinEta1 = Math.sin(eta1); 546 double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); 547 double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); 548 549 double anglePerSegment = sweep / numSegments; 550 for (int i = 0; i < numSegments; i++) { 551 double eta2 = eta1 + anglePerSegment; 552 double sinEta2 = Math.sin(eta2); 553 double cosEta2 = Math.cos(eta2); 554 double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); 555 double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); 556 double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; 557 double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; 558 double tanDiff2 = Math.tan((eta2 - eta1) / 2); 559 double alpha = Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; 560 double q1x = e1x + alpha * ep1x; 561 double q1y = e1y + alpha * ep1y; 562 double q2x = e2x - alpha * ep2x; 563 double q2y = e2y - alpha * ep2y; 564 565 p.cubicTo((float) q1x, (float) q1y, (float) q2x, (float) q2y, (float) e2x, (float) e2y); 566 eta1 = eta2; 567 e1x = e2x; 568 e1y = e2y; 569 ep1x = ep2x; 570 ep1y = ep2y; 571 } 572 } 573 } 574 575 @Implementation nCreatePathDataFromString(String pathString, int stringLength)576 protected static long nCreatePathDataFromString(String pathString, int stringLength) { 577 return 1; 578 } 579 580 @Implementation nCreateEmptyPathData()581 protected static long nCreateEmptyPathData() { 582 return 1; 583 } 584 585 @Implementation nCreatePathData(long nativePtr)586 protected static long nCreatePathData(long nativePtr) { 587 return 1; 588 } 589 } 590