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