1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN; 4 import static android.os.Build.VERSION_CODES.KITKAT; 5 import static android.os.Build.VERSION_CODES.LOLLIPOP; 6 import static org.robolectric.shadow.api.Shadow.extract; 7 import static org.robolectric.shadows.ShadowPath.Point.Type.LINE_TO; 8 import static org.robolectric.shadows.ShadowPath.Point.Type.MOVE_TO; 9 10 import android.graphics.Matrix; 11 import android.graphics.Path; 12 import android.graphics.Path.Direction; 13 import android.graphics.RectF; 14 import android.util.Log; 15 import java.awt.geom.AffineTransform; 16 import java.awt.geom.Arc2D; 17 import java.awt.geom.Area; 18 import java.awt.geom.Ellipse2D; 19 import java.awt.geom.GeneralPath; 20 import java.awt.geom.Path2D; 21 import java.awt.geom.PathIterator; 22 import java.awt.geom.Point2D; 23 import java.awt.geom.Rectangle2D; 24 import java.awt.geom.RoundRectangle2D; 25 import java.util.ArrayList; 26 import java.util.List; 27 import org.robolectric.annotation.Implementation; 28 import org.robolectric.annotation.Implements; 29 import org.robolectric.annotation.RealObject; 30 31 /** 32 * The shadow only supports straight-line paths. 33 */ 34 @SuppressWarnings({"UnusedDeclaration"}) 35 @Implements(Path.class) 36 public class ShadowPath { 37 private static final String TAG = ShadowPath.class.getSimpleName(); 38 private static final float EPSILON = 1e-4f; 39 40 @RealObject private Path realObject; 41 42 private List<Point> points = new ArrayList<>(); 43 private Point wasMovedTo; 44 45 private float mLastX = 0; 46 private float mLastY = 0; 47 private Path2D mPath = new Path2D.Double(); 48 private boolean mCachedIsEmpty = true; 49 private Path.FillType mFillType = Path.FillType.WINDING; 50 protected boolean isSimplePath; 51 52 @Implementation __constructor__(Path path)53 protected void __constructor__(Path path) { 54 ShadowPath shadowPath = extract(path); 55 points = new ArrayList<>(shadowPath.getPoints()); 56 } 57 getJavaShape()58 Path2D getJavaShape() { 59 return mPath; 60 } 61 62 @Implementation moveTo(float x, float y)63 protected void moveTo(float x, float y) { 64 mPath.moveTo(mLastX = x, mLastY = y); 65 66 // Legacy recording behavior 67 Point p = new Point(x, y, MOVE_TO); 68 points.add(p); 69 } 70 71 @Implementation lineTo(float x, float y)72 protected void lineTo(float x, float y) { 73 if (!hasPoints()) { 74 mPath.moveTo(mLastX = 0, mLastY = 0); 75 } 76 mPath.lineTo(mLastX = x, mLastY = y); 77 78 // Legacy recording behavior 79 Point point = new Point(x, y, LINE_TO); 80 points.add(point); 81 } 82 83 @Implementation quadTo(float x1, float y1, float x2, float y2)84 protected void quadTo(float x1, float y1, float x2, float y2) { 85 isSimplePath = false; 86 if (!hasPoints()) { 87 moveTo(0, 0); 88 } 89 mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2); 90 } 91 92 @Implementation cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)93 protected void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { 94 if (!hasPoints()) { 95 mPath.moveTo(0, 0); 96 } 97 mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3); 98 } 99 hasPoints()100 private boolean hasPoints() { 101 return !mPath.getPathIterator(null).isDone(); 102 } 103 104 @Implementation reset()105 protected void reset() { 106 mPath.reset(); 107 mLastX = 0; 108 mLastY = 0; 109 110 // Legacy recording behavior 111 points.clear(); 112 } 113 114 @Implementation(minSdk = LOLLIPOP) approximate(float acceptableError)115 protected float[] approximate(float acceptableError) { 116 PathIterator iterator = mPath.getPathIterator(null, acceptableError); 117 118 float segment[] = new float[6]; 119 float totalLength = 0; 120 ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>(); 121 Point2D.Float previousPoint = null; 122 while (!iterator.isDone()) { 123 int type = iterator.currentSegment(segment); 124 Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]); 125 // MoveTo shouldn't affect the length 126 if (previousPoint != null && type != PathIterator.SEG_MOVETO) { 127 totalLength += (float) currentPoint.distance(previousPoint); 128 } 129 previousPoint = currentPoint; 130 points.add(currentPoint); 131 iterator.next(); 132 } 133 134 int nPoints = points.size(); 135 float[] result = new float[nPoints * 3]; 136 previousPoint = null; 137 // Distance that we've covered so far. Used to calculate the fraction of the path that 138 // we've covered up to this point. 139 float walkedDistance = .0f; 140 for (int i = 0; i < nPoints; i++) { 141 Point2D.Float point = points.get(i); 142 float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f; 143 walkedDistance += distance; 144 result[i * 3] = walkedDistance / totalLength; 145 result[i * 3 + 1] = point.x; 146 result[i * 3 + 2] = point.y; 147 148 previousPoint = point; 149 } 150 151 return result; 152 } 153 154 /** 155 * @return all the points that have been added to the {@code Path} 156 */ getPoints()157 public List<Point> getPoints() { 158 return points; 159 } 160 161 public static class Point { 162 private final float x; 163 private final float y; 164 private final Type type; 165 166 public enum Type { 167 MOVE_TO, 168 LINE_TO 169 } 170 Point(float x, float y, Type type)171 public Point(float x, float y, Type type) { 172 this.x = x; 173 this.y = y; 174 this.type = type; 175 } 176 177 @Override equals(Object o)178 public boolean equals(Object o) { 179 if (this == o) return true; 180 if (!(o instanceof Point)) return false; 181 182 Point point = (Point) o; 183 184 if (Float.compare(point.x, x) != 0) return false; 185 if (Float.compare(point.y, y) != 0) return false; 186 if (type != point.type) return false; 187 188 return true; 189 } 190 191 @Override hashCode()192 public int hashCode() { 193 int result = (x != +0.0f ? Float.floatToIntBits(x) : 0); 194 result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0); 195 result = 31 * result + (type != null ? type.hashCode() : 0); 196 return result; 197 } 198 199 @Override toString()200 public String toString() { 201 return "Point(" + x + "," + y + "," + type + ")"; 202 } 203 getX()204 public float getX() { 205 return x; 206 } 207 getY()208 public float getY() { 209 return y; 210 } 211 getType()212 public Type getType() { 213 return type; 214 } 215 } 216 217 @Implementation rewind()218 protected void rewind() { 219 // call out to reset since there's nothing to optimize in 220 // terms of data structs. 221 reset(); 222 } 223 224 @Implementation set(Path src)225 protected void set(Path src) { 226 mPath.reset(); 227 228 ShadowPath shadowSrc = extract(src); 229 setFillType(shadowSrc.mFillType); 230 mPath.append(shadowSrc.mPath, false /*connect*/); 231 } 232 233 @Implementation(minSdk = KITKAT) op(Path path1, Path path2, Path.Op op)234 protected boolean op(Path path1, Path path2, Path.Op op) { 235 Log.w(TAG, "android.graphics.Path#op() not supported yet."); 236 return false; 237 } 238 239 @Implementation(minSdk = LOLLIPOP) isConvex()240 protected boolean isConvex() { 241 Log.w(TAG, "android.graphics.Path#isConvex() not supported yet."); 242 return true; 243 } 244 245 @Implementation getFillType()246 protected Path.FillType getFillType() { 247 return mFillType; 248 } 249 250 @Implementation setFillType(Path.FillType fillType)251 protected void setFillType(Path.FillType fillType) { 252 mFillType = fillType; 253 mPath.setWindingRule(getWindingRule(fillType)); 254 } 255 256 /** 257 * Returns the Java2D winding rules matching a given Android {@link FillType}. 258 * 259 * @param type the android fill type 260 * @return the matching java2d winding rule. 261 */ getWindingRule(Path.FillType type)262 private static int getWindingRule(Path.FillType type) { 263 switch (type) { 264 case WINDING: 265 case INVERSE_WINDING: 266 return GeneralPath.WIND_NON_ZERO; 267 case EVEN_ODD: 268 case INVERSE_EVEN_ODD: 269 return GeneralPath.WIND_EVEN_ODD; 270 271 default: 272 assert false; 273 return GeneralPath.WIND_NON_ZERO; 274 } 275 } 276 277 @Implementation isInverseFillType()278 protected boolean isInverseFillType() { 279 throw new UnsupportedOperationException("isInverseFillType"); 280 } 281 282 @Implementation toggleInverseFillType()283 protected void toggleInverseFillType() { 284 throw new UnsupportedOperationException("toggleInverseFillType"); 285 } 286 287 @Implementation isEmpty()288 protected boolean isEmpty() { 289 if (!mCachedIsEmpty) { 290 return false; 291 } 292 293 float[] coords = new float[6]; 294 mCachedIsEmpty = Boolean.TRUE; 295 for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) { 296 int type = it.currentSegment(coords); 297 // if (type != PathIterator.SEG_MOVETO) { 298 // Once we know that the path is not empty, we do not need to check again unless 299 // Path#reset is called. 300 mCachedIsEmpty = false; 301 return false; 302 // } 303 } 304 305 return true; 306 } 307 308 @Implementation isRect(RectF rect)309 protected boolean isRect(RectF rect) { 310 // create an Area that can test if the path is a rect 311 Area area = new Area(mPath); 312 if (area.isRectangular()) { 313 if (rect != null) { 314 fillBounds(rect); 315 } 316 317 return true; 318 } 319 320 return false; 321 } 322 323 @Implementation computeBounds(RectF bounds, boolean exact)324 protected void computeBounds(RectF bounds, boolean exact) { 325 fillBounds(bounds); 326 } 327 328 @Implementation incReserve(int extraPtCount)329 protected void incReserve(int extraPtCount) { 330 throw new UnsupportedOperationException("incReserve"); 331 } 332 333 @Implementation rMoveTo(float dx, float dy)334 protected void rMoveTo(float dx, float dy) { 335 dx += mLastX; 336 dy += mLastY; 337 mPath.moveTo(mLastX = dx, mLastY = dy); 338 } 339 340 @Implementation rLineTo(float dx, float dy)341 protected void rLineTo(float dx, float dy) { 342 if (!hasPoints()) { 343 mPath.moveTo(mLastX = 0, mLastY = 0); 344 } 345 346 if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) { 347 // The delta is so small that this shouldn't generate a line 348 return; 349 } 350 351 dx += mLastX; 352 dy += mLastY; 353 mPath.lineTo(mLastX = dx, mLastY = dy); 354 } 355 356 @Implementation rQuadTo(float dx1, float dy1, float dx2, float dy2)357 protected void rQuadTo(float dx1, float dy1, float dx2, float dy2) { 358 if (!hasPoints()) { 359 mPath.moveTo(mLastX = 0, mLastY = 0); 360 } 361 dx1 += mLastX; 362 dy1 += mLastY; 363 dx2 += mLastX; 364 dy2 += mLastY; 365 mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2); 366 } 367 368 @Implementation rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)369 protected void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { 370 if (!hasPoints()) { 371 mPath.moveTo(mLastX = 0, mLastY = 0); 372 } 373 x1 += mLastX; 374 y1 += mLastY; 375 x2 += mLastX; 376 y2 += mLastY; 377 x3 += mLastX; 378 y3 += mLastY; 379 mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3); 380 } 381 382 @Implementation arcTo(RectF oval, float startAngle, float sweepAngle)383 protected void arcTo(RectF oval, float startAngle, float sweepAngle) { 384 arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false); 385 } 386 387 @Implementation arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)388 protected void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) { 389 arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo); 390 } 391 392 @Implementation(minSdk = LOLLIPOP) arcTo( float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)393 protected void arcTo( 394 float left, 395 float top, 396 float right, 397 float bottom, 398 float startAngle, 399 float sweepAngle, 400 boolean forceMoveTo) { 401 isSimplePath = false; 402 Arc2D arc = 403 new Arc2D.Float( 404 left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN); 405 mPath.append(arc, true /*connect*/); 406 407 resetLastPointFromPath(); 408 } 409 410 @Implementation close()411 protected void close() { 412 if (!hasPoints()) { 413 mPath.moveTo(mLastX = 0, mLastY = 0); 414 } 415 mPath.closePath(); 416 } 417 418 @Implementation addRect(RectF rect, Direction dir)419 protected void addRect(RectF rect, Direction dir) { 420 addRect(rect.left, rect.top, rect.right, rect.bottom, dir); 421 } 422 423 @Implementation addRect(float left, float top, float right, float bottom, Path.Direction dir)424 protected void addRect(float left, float top, float right, float bottom, Path.Direction dir) { 425 moveTo(left, top); 426 427 switch (dir) { 428 case CW: 429 lineTo(right, top); 430 lineTo(right, bottom); 431 lineTo(left, bottom); 432 break; 433 case CCW: 434 lineTo(left, bottom); 435 lineTo(right, bottom); 436 lineTo(right, top); 437 break; 438 } 439 440 close(); 441 442 resetLastPointFromPath(); 443 } 444 445 @Implementation(minSdk = LOLLIPOP) addOval(float left, float top, float right, float bottom, Path.Direction dir)446 protected void addOval(float left, float top, float right, float bottom, Path.Direction dir) { 447 mPath.append(new Ellipse2D.Float(left, top, right - left, bottom - top), false); 448 } 449 450 @Implementation addCircle(float x, float y, float radius, Path.Direction dir)451 protected void addCircle(float x, float y, float radius, Path.Direction dir) { 452 mPath.append(new Ellipse2D.Float(x - radius, y - radius, radius * 2, radius * 2), false); 453 } 454 455 @Implementation(minSdk = LOLLIPOP) addArc( float left, float top, float right, float bottom, float startAngle, float sweepAngle)456 protected void addArc( 457 float left, float top, float right, float bottom, float startAngle, float sweepAngle) { 458 mPath.append( 459 new Arc2D.Float( 460 left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN), 461 false); 462 } 463 464 @Implementation(minSdk = JELLY_BEAN) addRoundRect(RectF rect, float rx, float ry, Direction dir)465 protected void addRoundRect(RectF rect, float rx, float ry, Direction dir) { 466 addRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, dir); 467 } 468 469 @Implementation(minSdk = JELLY_BEAN) addRoundRect(RectF rect, float[] radii, Direction dir)470 protected void addRoundRect(RectF rect, float[] radii, Direction dir) { 471 if (rect == null) { 472 throw new NullPointerException("need rect parameter"); 473 } 474 addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir); 475 } 476 477 @Implementation(minSdk = LOLLIPOP) addRoundRect( float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir)478 protected void addRoundRect( 479 float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) { 480 mPath.append( 481 new RoundRectangle2D.Float(left, top, right - left, bottom - top, rx * 2, ry * 2), false); 482 } 483 484 @Implementation(minSdk = LOLLIPOP) addRoundRect( float left, float top, float right, float bottom, float[] radii, Path.Direction dir)485 protected void addRoundRect( 486 float left, float top, float right, float bottom, float[] radii, Path.Direction dir) { 487 if (radii.length < 8) { 488 throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values"); 489 } 490 isSimplePath = false; 491 492 float[] cornerDimensions = new float[radii.length]; 493 for (int i = 0; i < radii.length; i++) { 494 cornerDimensions[i] = 2 * radii[i]; 495 } 496 mPath.append( 497 new RoundRectangle(left, top, right - left, bottom - top, cornerDimensions), false); 498 } 499 500 @Implementation addPath(Path src, float dx, float dy)501 protected void addPath(Path src, float dx, float dy) { 502 isSimplePath = false; 503 ShadowPath.addPath(realObject, src, AffineTransform.getTranslateInstance(dx, dy)); 504 } 505 506 @Implementation addPath(Path src)507 protected void addPath(Path src) { 508 isSimplePath = false; 509 ShadowPath.addPath(realObject, src, null); 510 } 511 512 @Implementation addPath(Path src, Matrix matrix)513 protected void addPath(Path src, Matrix matrix) { 514 if (matrix == null) { 515 return; 516 } 517 ShadowPath shadowSrc = extract(src); 518 if (!shadowSrc.isSimplePath) isSimplePath = false; 519 520 ShadowMatrix shadowMatrix = extract(matrix); 521 ShadowPath.addPath(realObject, src, shadowMatrix.getAffineTransform()); 522 } 523 addPath(Path destPath, Path srcPath, AffineTransform transform)524 private static void addPath(Path destPath, Path srcPath, AffineTransform transform) { 525 if (destPath == null) { 526 return; 527 } 528 529 if (srcPath == null) { 530 return; 531 } 532 533 ShadowPath shadowDestPath = extract(destPath); 534 ShadowPath shadowSrcPath = extract(srcPath); 535 if (transform != null) { 536 shadowDestPath.mPath.append(shadowSrcPath.mPath.getPathIterator(transform), false); 537 } else { 538 shadowDestPath.mPath.append(shadowSrcPath.mPath, false); 539 } 540 } 541 542 @Implementation offset(float dx, float dy, Path dst)543 protected void offset(float dx, float dy, Path dst) { 544 if (dst != null) { 545 dst.set(realObject); 546 } else { 547 dst = realObject; 548 } 549 dst.offset(dx, dy); 550 } 551 552 @Implementation offset(float dx, float dy)553 protected void offset(float dx, float dy) { 554 GeneralPath newPath = new GeneralPath(); 555 556 PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy)); 557 558 newPath.append(iterator, false /*connect*/); 559 mPath = newPath; 560 } 561 562 @Implementation setLastPoint(float dx, float dy)563 protected void setLastPoint(float dx, float dy) { 564 mLastX = dx; 565 mLastY = dy; 566 } 567 568 @Implementation transform(Matrix matrix, Path dst)569 protected void transform(Matrix matrix, Path dst) { 570 ShadowMatrix shadowMatrix = extract(matrix); 571 572 if (shadowMatrix.hasPerspective()) { 573 Log.w(TAG, "android.graphics.Path#transform() only supports affine transformations."); 574 } 575 576 GeneralPath newPath = new GeneralPath(); 577 578 PathIterator iterator = mPath.getPathIterator(shadowMatrix.getAffineTransform()); 579 newPath.append(iterator, false /*connect*/); 580 581 if (dst != null) { 582 ShadowPath shadowPath = extract(dst); 583 shadowPath.mPath = newPath; 584 } else { 585 mPath = newPath; 586 } 587 } 588 589 @Implementation transform(Matrix matrix)590 protected void transform(Matrix matrix) { 591 transform(matrix, null); 592 } 593 594 /** 595 * Fills the given {@link RectF} with the path bounds. 596 * 597 * @param bounds the RectF to be filled. 598 */ fillBounds(RectF bounds)599 public void fillBounds(RectF bounds) { 600 Rectangle2D rect = mPath.getBounds2D(); 601 bounds.left = (float) rect.getMinX(); 602 bounds.right = (float) rect.getMaxX(); 603 bounds.top = (float) rect.getMinY(); 604 bounds.bottom = (float) rect.getMaxY(); 605 } 606 resetLastPointFromPath()607 private void resetLastPointFromPath() { 608 Point2D last = mPath.getCurrentPoint(); 609 mLastX = (float) last.getX(); 610 mLastY = (float) last.getY(); 611 } 612 } 613