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