Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import java.awt.geom.AffineTransform;
      4 import java.awt.geom.PathIterator;
      5 import java.awt.geom.Rectangle2D;
      6 import java.awt.geom.RectangularShape;
      7 import java.awt.geom.RoundRectangle2D;
      8 import java.util.EnumSet;
      9 import java.util.NoSuchElementException;
     10 
     11 /**
     12  * Defines a rectangle with rounded corners, where the sizes of the corners are potentially
     13  * different.
     14  *
     15  * <p>Copied from
     16  * https://github.com/aosp-mirror/platform_frameworks_base/blob/oreo-release/tools/layoutlib/bridge/src/android/graphics/RoundRectangle.java
     17  */
     18 public class RoundRectangle extends RectangularShape {
     19   public double x;
     20   public double y;
     21   public double width;
     22   public double height;
     23   public double ulWidth;
     24   public double ulHeight;
     25   public double urWidth;
     26   public double urHeight;
     27   public double lrWidth;
     28   public double lrHeight;
     29   public double llWidth;
     30   public double llHeight;
     31 
     32   private enum Zone {
     33     CLOSE_OUTSIDE,
     34     CLOSE_INSIDE,
     35     MIDDLE,
     36     FAR_INSIDE,
     37     FAR_OUTSIDE
     38   }
     39 
     40   private final EnumSet<Zone> close = EnumSet.of(Zone.CLOSE_OUTSIDE, Zone.CLOSE_INSIDE);
     41   private final EnumSet<Zone> far = EnumSet.of(Zone.FAR_OUTSIDE, Zone.FAR_INSIDE);
     42 
     43   /**
     44    * @param cornerDimensions array of 8 floating-point number corresponding to the width and the
     45    *     height of each corner in the following order: upper-left, upper-right, lower-right,
     46    *     lower-left. It assumes for the size the same convention as {@link RoundRectangle2D}, that
     47    *     is that the width and height of a corner correspond to the total width and height of the
     48    *     ellipse that corner is a quarter of.
     49    */
     50   public RoundRectangle(float x, float y, float width, float height, float[] cornerDimensions) {
     51     assert cornerDimensions.length == 8
     52         : "The array of corner dimensions must have eight " + "elements";
     53 
     54     this.x = x;
     55     this.y = y;
     56     this.width = width;
     57     this.height = height;
     58 
     59     float[] dimensions = cornerDimensions.clone();
     60     // If a value is negative, the corresponding corner is squared
     61     for (int i = 0; i < dimensions.length; i += 2) {
     62       if (dimensions[i] < 0 || dimensions[i + 1] < 0) {
     63         dimensions[i] = 0;
     64         dimensions[i + 1] = 0;
     65       }
     66     }
     67 
     68     double topCornerWidth = (dimensions[0] + dimensions[2]) / 2d;
     69     double bottomCornerWidth = (dimensions[4] + dimensions[6]) / 2d;
     70     double leftCornerHeight = (dimensions[1] + dimensions[7]) / 2d;
     71     double rightCornerHeight = (dimensions[3] + dimensions[5]) / 2d;
     72 
     73     // Rescale the corner dimensions if they are bigger than the rectangle
     74     double scale = Math.min(1.0, width / topCornerWidth);
     75     scale = Math.min(scale, width / bottomCornerWidth);
     76     scale = Math.min(scale, height / leftCornerHeight);
     77     scale = Math.min(scale, height / rightCornerHeight);
     78 
     79     this.ulWidth = dimensions[0] * scale;
     80     this.ulHeight = dimensions[1] * scale;
     81     this.urWidth = dimensions[2] * scale;
     82     this.urHeight = dimensions[3] * scale;
     83     this.lrWidth = dimensions[4] * scale;
     84     this.lrHeight = dimensions[5] * scale;
     85     this.llWidth = dimensions[6] * scale;
     86     this.llHeight = dimensions[7] * scale;
     87   }
     88 
     89   @Override
     90   public double getX() {
     91     return x;
     92   }
     93 
     94   @Override
     95   public double getY() {
     96     return y;
     97   }
     98 
     99   @Override
    100   public double getWidth() {
    101     return width;
    102   }
    103 
    104   @Override
    105   public double getHeight() {
    106     return height;
    107   }
    108 
    109   @Override
    110   public boolean isEmpty() {
    111     return (width <= 0d) || (height <= 0d);
    112   }
    113 
    114   @Override
    115   public void setFrame(double x, double y, double w, double h) {
    116     this.x = x;
    117     this.y = y;
    118     this.width = w;
    119     this.height = h;
    120   }
    121 
    122   @Override
    123   public Rectangle2D getBounds2D() {
    124     return new Rectangle2D.Double(x, y, width, height);
    125   }
    126 
    127   @Override
    128   public boolean contains(double x, double y) {
    129     if (isEmpty()) {
    130       return false;
    131     }
    132 
    133     double x0 = getX();
    134     double y0 = getY();
    135     double x1 = x0 + getWidth();
    136     double y1 = y0 + getHeight();
    137     // Check for trivial rejection - point is outside bounding rectangle
    138     if (x < x0 || y < y0 || x >= x1 || y >= y1) {
    139       return false;
    140     }
    141 
    142     double insideTopX0 = x0 + ulWidth / 2d;
    143     double insideLeftY0 = y0 + ulHeight / 2d;
    144     if (x < insideTopX0 && y < insideLeftY0) {
    145       // In the upper-left corner
    146       return isInsideCorner(x - insideTopX0, y - insideLeftY0, ulWidth / 2d, ulHeight / 2d);
    147     }
    148 
    149     double insideTopX1 = x1 - urWidth / 2d;
    150     double insideRightY0 = y0 + urHeight / 2d;
    151     if (x > insideTopX1 && y < insideRightY0) {
    152       // In the upper-right corner
    153       return isInsideCorner(x - insideTopX1, y - insideRightY0, urWidth / 2d, urHeight / 2d);
    154     }
    155 
    156     double insideBottomX1 = x1 - lrWidth / 2d;
    157     double insideRightY1 = y1 - lrHeight / 2d;
    158     if (x > insideBottomX1 && y > insideRightY1) {
    159       // In the lower-right corner
    160       return isInsideCorner(x - insideBottomX1, y - insideRightY1, lrWidth / 2d, lrHeight / 2d);
    161     }
    162 
    163     double insideBottomX0 = x0 + llWidth / 2d;
    164     double insideLeftY1 = y1 - llHeight / 2d;
    165     if (x < insideBottomX0 && y > insideLeftY1) {
    166       // In the lower-left corner
    167       return isInsideCorner(x - insideBottomX0, y - insideLeftY1, llWidth / 2d, llHeight / 2d);
    168     }
    169 
    170     // In the central part of the rectangle
    171     return true;
    172   }
    173 
    174   private boolean isInsideCorner(double x, double y, double width, double height) {
    175     double squareDist = height * height * x * x + width * width * y * y;
    176     return squareDist <= width * width * height * height;
    177   }
    178 
    179   private Zone classify(
    180       double coord, double side1, double arcSize1, double side2, double arcSize2) {
    181     if (coord < side1) {
    182       return Zone.CLOSE_OUTSIDE;
    183     } else if (coord < side1 + arcSize1) {
    184       return Zone.CLOSE_INSIDE;
    185     } else if (coord < side2 - arcSize2) {
    186       return Zone.MIDDLE;
    187     } else if (coord < side2) {
    188       return Zone.FAR_INSIDE;
    189     } else {
    190       return Zone.FAR_OUTSIDE;
    191     }
    192   }
    193 
    194   @Override
    195   public boolean intersects(double x, double y, double w, double h) {
    196     if (isEmpty() || w <= 0 || h <= 0) {
    197       return false;
    198     }
    199     double x0 = getX();
    200     double y0 = getY();
    201     double x1 = x0 + getWidth();
    202     double y1 = y0 + getHeight();
    203     // Check for trivial rejection - bounding rectangles do not intersect
    204     if (x + w <= x0 || x >= x1 || y + h <= y0 || y >= y1) {
    205       return false;
    206     }
    207 
    208     double maxLeftCornerWidth = Math.max(ulWidth, llWidth) / 2d;
    209     double maxRightCornerWidth = Math.max(urWidth, lrWidth) / 2d;
    210     double maxUpperCornerHeight = Math.max(ulHeight, urHeight) / 2d;
    211     double maxLowerCornerHeight = Math.max(llHeight, lrHeight) / 2d;
    212     Zone x0class = classify(x, x0, maxLeftCornerWidth, x1, maxRightCornerWidth);
    213     Zone x1class = classify(x + w, x0, maxLeftCornerWidth, x1, maxRightCornerWidth);
    214     Zone y0class = classify(y, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight);
    215     Zone y1class = classify(y + h, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight);
    216 
    217     // Trivially accept if any point is inside inner rectangle
    218     if (x0class == Zone.MIDDLE
    219         || x1class == Zone.MIDDLE
    220         || y0class == Zone.MIDDLE
    221         || y1class == Zone.MIDDLE) {
    222       return true;
    223     }
    224     // Trivially accept if either edge spans inner rectangle
    225     if ((close.contains(x0class) && far.contains(x1class))
    226         || (close.contains(y0class) && far.contains(y1class))) {
    227       return true;
    228     }
    229 
    230     // Since neither edge spans the center, then one of the corners
    231     // must be in one of the rounded edges.  We detect this case if
    232     // a [xy]0class is 3 or a [xy]1class is 1.  One of those two cases
    233     // must be true for each direction.
    234     // We now find a "nearest point" to test for being inside a rounded
    235     // corner.
    236     if (x1class == Zone.CLOSE_INSIDE && y1class == Zone.CLOSE_INSIDE) {
    237       // Potentially in upper-left corner
    238       x = x + w - x0 - ulWidth / 2d;
    239       y = y + h - y0 - ulHeight / 2d;
    240       return x > 0 || y > 0 || isInsideCorner(x, y, ulWidth / 2d, ulHeight / 2d);
    241     }
    242     if (x1class == Zone.CLOSE_INSIDE) {
    243       // Potentially in lower-left corner
    244       x = x + w - x0 - llWidth / 2d;
    245       y = y - y1 + llHeight / 2d;
    246       return x > 0 || y < 0 || isInsideCorner(x, y, llWidth / 2d, llHeight / 2d);
    247     }
    248     if (y1class == Zone.CLOSE_INSIDE) {
    249       // Potentially in the upper-right corner
    250       x = x - x1 + urWidth / 2d;
    251       y = y + h - y0 - urHeight / 2d;
    252       return x < 0 || y > 0 || isInsideCorner(x, y, urWidth / 2d, urHeight / 2d);
    253     }
    254     // Potentially in the lower-right corner
    255     x = x - x1 + lrWidth / 2d;
    256     y = y - y1 + lrHeight / 2d;
    257     return x < 0 || y < 0 || isInsideCorner(x, y, lrWidth / 2d, lrHeight / 2d);
    258   }
    259 
    260   @Override
    261   public boolean contains(double x, double y, double w, double h) {
    262     if (isEmpty() || w <= 0 || h <= 0) {
    263       return false;
    264     }
    265     return (contains(x, y) && contains(x + w, y) && contains(x, y + h) && contains(x + w, y + h));
    266   }
    267 
    268   @Override
    269   public PathIterator getPathIterator(final AffineTransform at) {
    270     return new PathIterator() {
    271       int index;
    272 
    273       // ArcIterator.btan(Math.PI/2)
    274       public static final double CtrlVal = 0.5522847498307933;
    275       private final double ncv = 1.0 - CtrlVal;
    276 
    277       // Coordinates of control points for Bezier curves approximating the straight lines
    278       // and corners of the rounded rectangle.
    279       private final double[][] ctrlpts = {
    280         {0.0, 0.0, 0.0, ulHeight},
    281         {0.0, 0.0, 1.0, -llHeight},
    282         {0.0, 0.0, 1.0, -llHeight * ncv, 0.0, ncv * llWidth, 1.0, 0.0, 0.0, llWidth, 1.0, 0.0},
    283         {1.0, -lrWidth, 1.0, 0.0},
    284         {1.0, -lrWidth * ncv, 1.0, 0.0, 1.0, 0.0, 1.0, -lrHeight * ncv, 1.0, 0.0, 1.0, -lrHeight},
    285         {1.0, 0.0, 0.0, urHeight},
    286         {1.0, 0.0, 0.0, ncv * urHeight, 1.0, -urWidth * ncv, 0.0, 0.0, 1.0, -urWidth, 0.0, 0.0},
    287         {0.0, ulWidth, 0.0, 0.0},
    288         {0.0, ncv * ulWidth, 0.0, 0.0, 0.0, 0.0, 0.0, ncv * ulHeight, 0.0, 0.0, 0.0, ulHeight},
    289         {}
    290       };
    291       private final int[] types = {
    292         SEG_MOVETO,
    293         SEG_LINETO,
    294         SEG_CUBICTO,
    295         SEG_LINETO,
    296         SEG_CUBICTO,
    297         SEG_LINETO,
    298         SEG_CUBICTO,
    299         SEG_LINETO,
    300         SEG_CUBICTO,
    301         SEG_CLOSE,
    302       };
    303 
    304       @Override
    305       public int getWindingRule() {
    306         return WIND_NON_ZERO;
    307       }
    308 
    309       @Override
    310       public boolean isDone() {
    311         return index >= ctrlpts.length;
    312       }
    313 
    314       @Override
    315       public void next() {
    316         index++;
    317       }
    318 
    319       @Override
    320       public int currentSegment(float[] coords) {
    321         if (isDone()) {
    322           throw new NoSuchElementException("roundrect iterator out of bounds");
    323         }
    324         int nc = 0;
    325         double ctrls[] = ctrlpts[index];
    326         for (int i = 0; i < ctrls.length; i += 4) {
    327           coords[nc++] = (float) (x + ctrls[i] * width + ctrls[i + 1] / 2d);
    328           coords[nc++] = (float) (y + ctrls[i + 2] * height + ctrls[i + 3] / 2d);
    329         }
    330         if (at != null) {
    331           at.transform(coords, 0, coords, 0, nc / 2);
    332         }
    333         return types[index];
    334       }
    335 
    336       @Override
    337       public int currentSegment(double[] coords) {
    338         if (isDone()) {
    339           throw new NoSuchElementException("roundrect iterator out of bounds");
    340         }
    341         int nc = 0;
    342         double ctrls[] = ctrlpts[index];
    343         for (int i = 0; i < ctrls.length; i += 4) {
    344           coords[nc++] = x + ctrls[i] * width + ctrls[i + 1] / 2d;
    345           coords[nc++] = y + ctrls[i + 2] * height + ctrls[i + 3] / 2d;
    346         }
    347         if (at != null) {
    348           at.transform(coords, 0, coords, 0, nc / 2);
    349         }
    350         return types[index];
    351       }
    352     };
    353   }
    354 }
    355