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