Home | History | Annotate | Download | only in shadows
      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
     53   protected void __constructor__(Path path) {
     54     ShadowPath shadowPath = extract(path);
     55     points = new ArrayList<>(shadowPath.getPoints());
     56   }
     57 
     58   Path2D getJavaShape() {
     59     return mPath;
     60   }
     61 
     62   @Implementation
     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
     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
     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
     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 
    100   private boolean hasPoints() {
    101     return !mPath.getPathIterator(null).isDone();
    102   }
    103 
    104   @Implementation
    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)
    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    */
    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 
    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
    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
    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
    200     public String toString() {
    201       return "Point(" + x + "," + y + "," + type + ")";
    202     }
    203 
    204     public float getX() {
    205       return x;
    206     }
    207 
    208     public float getY() {
    209       return y;
    210     }
    211 
    212     public Type getType() {
    213       return type;
    214     }
    215   }
    216 
    217   @Implementation
    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
    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)
    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)
    240   protected boolean isConvex() {
    241     Log.w(TAG, "android.graphics.Path#isConvex() not supported yet.");
    242     return true;
    243   }
    244 
    245   @Implementation
    246   protected Path.FillType getFillType() {
    247     return mFillType;
    248   }
    249 
    250   @Implementation
    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    */
    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
    278   protected boolean isInverseFillType() {
    279     throw new UnsupportedOperationException("isInverseFillType");
    280   }
    281 
    282   @Implementation
    283   protected void toggleInverseFillType() {
    284     throw new UnsupportedOperationException("toggleInverseFillType");
    285   }
    286 
    287   @Implementation
    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
    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
    324   protected void computeBounds(RectF bounds, boolean exact) {
    325     fillBounds(bounds);
    326   }
    327 
    328   @Implementation
    329   protected void incReserve(int extraPtCount) {
    330     throw new UnsupportedOperationException("incReserve");
    331   }
    332 
    333   @Implementation
    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
    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
    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
    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
    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
    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)
    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
    411   protected void close() {
    412     if (!hasPoints()) {
    413       mPath.moveTo(mLastX = 0, mLastY = 0);
    414     }
    415     mPath.closePath();
    416   }
    417 
    418   @Implementation
    419   protected void addRect(RectF rect, Direction dir) {
    420     addRect(rect.left, rect.top, rect.right, rect.bottom, dir);
    421   }
    422 
    423   @Implementation
    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)
    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
    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)
    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)
    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)
    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)
    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)
    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
    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
    507   protected void addPath(Path src) {
    508     isSimplePath = false;
    509     ShadowPath.addPath(realObject, src, null);
    510   }
    511 
    512   @Implementation
    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 
    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
    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
    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
    563   protected void setLastPoint(float dx, float dy) {
    564     mLastX = dx;
    565     mLastY = dy;
    566   }
    567 
    568   @Implementation
    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
    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    */
    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 
    607   private void resetLastPointFromPath() {
    608     Point2D last = mPath.getCurrentPoint();
    609     mLastX = (float) last.getX();
    610     mLastY = (float) last.getY();
    611   }
    612 }
    613