• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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