• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    */
RoundRectangle(float x, float y, float width, float height, float[] cornerDimensions)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
getX()90   public double getX() {
91     return x;
92   }
93 
94   @Override
getY()95   public double getY() {
96     return y;
97   }
98 
99   @Override
getWidth()100   public double getWidth() {
101     return width;
102   }
103 
104   @Override
getHeight()105   public double getHeight() {
106     return height;
107   }
108 
109   @Override
isEmpty()110   public boolean isEmpty() {
111     return (width <= 0d) || (height <= 0d);
112   }
113 
114   @Override
setFrame(double x, double y, double w, double h)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
getBounds2D()123   public Rectangle2D getBounds2D() {
124     return new Rectangle2D.Double(x, y, width, height);
125   }
126 
127   @Override
contains(double x, double y)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 
isInsideCorner(double x, double y, double width, double height)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 
classify( double coord, double side1, double arcSize1, double side2, double arcSize2)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
intersects(double x, double y, double w, double h)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
contains(double x, double y, double w, double h)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
getPathIterator(final AffineTransform at)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