• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         if (cornerDimensions.length != 8) {
65             throw new IllegalArgumentException("The array of corner dimensions must have eight " +
66                     "elements");
67         }
68 
69         this.x = x;
70         this.y = y;
71         this.width = width;
72         this.height = height;
73 
74         float[] dimensions = cornerDimensions.clone();
75         // If a value is negative, the corresponding corner is squared
76         for (int i = 0; i < dimensions.length; i += 2) {
77             if (dimensions[i] < 0 || dimensions[i + 1] < 0) {
78                 dimensions[i] = 0;
79                 dimensions[i + 1] = 0;
80             }
81         }
82 
83         double topCornerWidth = (dimensions[0] + dimensions[2]) / 2d;
84         double bottomCornerWidth = (dimensions[4] + dimensions[6]) / 2d;
85         double leftCornerHeight = (dimensions[1] + dimensions[7]) / 2d;
86         double rightCornerHeight = (dimensions[3] + dimensions[5]) / 2d;
87 
88         // Rescale the corner dimensions if they are bigger than the rectangle
89         double scale = Math.min(1.0, width / topCornerWidth);
90         scale = Math.min(scale, width / bottomCornerWidth);
91         scale = Math.min(scale, height / leftCornerHeight);
92         scale = Math.min(scale, height / rightCornerHeight);
93 
94         this.ulWidth = dimensions[0] * scale;
95         this.ulHeight = dimensions[1] * scale;
96         this.urWidth = dimensions[2] * scale;
97         this.urHeight = dimensions[3] * scale;
98         this.lrWidth = dimensions[4] * scale;
99         this.lrHeight = dimensions[5] * scale;
100         this.llWidth = dimensions[6] * scale;
101         this.llHeight = dimensions[7] * scale;
102     }
103 
104     @Override
getX()105     public double getX() {
106         return x;
107     }
108 
109     @Override
getY()110     public double getY() {
111         return y;
112     }
113 
114     @Override
getWidth()115     public double getWidth() {
116         return width;
117     }
118 
119     @Override
getHeight()120     public double getHeight() {
121         return height;
122     }
123 
124     @Override
isEmpty()125     public boolean isEmpty() {
126         return (width <= 0d) || (height <= 0d);
127     }
128 
129     @Override
setFrame(double x, double y, double w, double h)130     public void setFrame(double x, double y, double w, double h) {
131         this.x = x;
132         this.y = y;
133         this.width = w;
134         this.height = h;
135     }
136 
137     @Override
getBounds2D()138     public Rectangle2D getBounds2D() {
139         return new Rectangle2D.Double(x, y, width, height);
140     }
141 
142     @Override
contains(double x, double y)143     public boolean contains(double x, double y) {
144         if (isEmpty()) {
145             return false;
146         }
147 
148         double x0 = getX();
149         double y0 = getY();
150         double x1 = x0 + getWidth();
151         double y1 = y0 + getHeight();
152         // Check for trivial rejection - point is outside bounding rectangle
153         if (x < x0 || y < y0 || x >= x1 || y >= y1) {
154             return false;
155         }
156 
157         double insideTopX0 = x0 + ulWidth / 2d;
158         double insideLeftY0 = y0 + ulHeight / 2d;
159         if (x < insideTopX0 && y < insideLeftY0) {
160             // In the upper-left corner
161             return isInsideCorner(x - insideTopX0, y - insideLeftY0, ulWidth / 2d, ulHeight / 2d);
162         }
163 
164         double insideTopX1 = x1 - urWidth / 2d;
165         double insideRightY0 = y0 + urHeight / 2d;
166         if (x > insideTopX1 && y < insideRightY0) {
167             // In the upper-right corner
168             return isInsideCorner(x - insideTopX1, y - insideRightY0, urWidth / 2d, urHeight / 2d);
169         }
170 
171         double insideBottomX1 = x1 - lrWidth / 2d;
172         double insideRightY1 = y1 - lrHeight / 2d;
173         if (x > insideBottomX1 && y > insideRightY1) {
174             // In the lower-right corner
175             return isInsideCorner(x - insideBottomX1, y - insideRightY1, lrWidth / 2d,
176                     lrHeight / 2d);
177         }
178 
179         double insideBottomX0 = x0 + llWidth / 2d;
180         double insideLeftY1 = y1 - llHeight / 2d;
181         if (x < insideBottomX0 && y > insideLeftY1) {
182             // In the lower-left corner
183             return isInsideCorner(x - insideBottomX0, y - insideLeftY1, llWidth / 2d,
184                     llHeight / 2d);
185         }
186 
187         // In the central part of the rectangle
188         return true;
189     }
190 
isInsideCorner(double x, double y, double width, double height)191     private boolean isInsideCorner(double x, double y, double width, double height) {
192         double squareDist = height * height * x * x + width * width * y * y;
193         return squareDist <= width * width * height * height;
194     }
195 
classify(double coord, double side1, double arcSize1, double side2, double arcSize2)196     private Zone classify(double coord, double side1, double arcSize1, double side2,
197             double arcSize2) {
198         if (coord < side1) {
199             return Zone.CLOSE_OUTSIDE;
200         } else if (coord < side1 + arcSize1) {
201             return Zone.CLOSE_INSIDE;
202         } else if (coord < side2 - arcSize2) {
203             return Zone.MIDDLE;
204         } else if (coord < side2) {
205             return Zone.FAR_INSIDE;
206         } else {
207             return Zone.FAR_OUTSIDE;
208         }
209     }
210 
intersects(double x, double y, double w, double h)211     public boolean intersects(double x, double y, double w, double h) {
212         if (isEmpty() || w <= 0 || h <= 0) {
213             return false;
214         }
215         double x0 = getX();
216         double y0 = getY();
217         double x1 = x0 + getWidth();
218         double y1 = y0 + getHeight();
219         // Check for trivial rejection - bounding rectangles do not intersect
220         if (x + w <= x0 || x >= x1 || y + h <= y0 || y >= y1) {
221             return false;
222         }
223 
224         double maxLeftCornerWidth = Math.max(ulWidth, llWidth) / 2d;
225         double maxRightCornerWidth = Math.max(urWidth, lrWidth) / 2d;
226         double maxUpperCornerHeight = Math.max(ulHeight, urHeight) / 2d;
227         double maxLowerCornerHeight = Math.max(llHeight, lrHeight) / 2d;
228         Zone x0class = classify(x, x0, maxLeftCornerWidth, x1, maxRightCornerWidth);
229         Zone x1class = classify(x + w, x0, maxLeftCornerWidth, x1, maxRightCornerWidth);
230         Zone y0class = classify(y, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight);
231         Zone y1class = classify(y + h, y0, maxUpperCornerHeight, y1, maxLowerCornerHeight);
232 
233         // Trivially accept if any point is inside inner rectangle
234         if (x0class == Zone.MIDDLE || x1class == Zone.MIDDLE || y0class == Zone.MIDDLE || y1class == Zone.MIDDLE) {
235             return true;
236         }
237         // Trivially accept if either edge spans inner rectangle
238         if ((close.contains(x0class) && far.contains(x1class)) || (close.contains(y0class) &&
239                 far.contains(y1class))) {
240             return true;
241         }
242 
243         // Since neither edge spans the center, then one of the corners
244         // must be in one of the rounded edges.  We detect this case if
245         // a [xy]0class is 3 or a [xy]1class is 1.  One of those two cases
246         // must be true for each direction.
247         // We now find a "nearest point" to test for being inside a rounded
248         // corner.
249         if (x1class == Zone.CLOSE_INSIDE && y1class == Zone.CLOSE_INSIDE) {
250             // Potentially in upper-left corner
251             x = x + w - x0 - ulWidth / 2d;
252             y = y + h - y0 - ulHeight / 2d;
253             return x > 0 || y > 0 || isInsideCorner(x, y, ulWidth / 2d, ulHeight / 2d);
254         }
255         if (x1class == Zone.CLOSE_INSIDE) {
256             // Potentially in lower-left corner
257             x = x + w - x0 - llWidth / 2d;
258             y = y - y1 + llHeight / 2d;
259             return x > 0 || y < 0 || isInsideCorner(x, y, llWidth / 2d, llHeight / 2d);
260         }
261         if (y1class == Zone.CLOSE_INSIDE) {
262             //Potentially in the upper-right corner
263             x = x - x1 + urWidth / 2d;
264             y = y + h - y0 - urHeight / 2d;
265             return x < 0 || y > 0 || isInsideCorner(x, y, urWidth / 2d, urHeight / 2d);
266         }
267         // Potentially in the lower-right corner
268         x = x - x1 + lrWidth / 2d;
269         y = y - y1 + lrHeight / 2d;
270         return x < 0 || y < 0 || isInsideCorner(x, y, lrWidth / 2d, lrHeight / 2d);
271     }
272 
273     @Override
contains(double x, double y, double w, double h)274     public boolean contains(double x, double y, double w, double h) {
275         if (isEmpty() || w <= 0 || h <= 0) {
276             return false;
277         }
278         return (contains(x, y) &&
279                 contains(x + w, y) &&
280                 contains(x, y + h) &&
281                 contains(x + w, y + h));
282     }
283 
284     @Override
getPathIterator(final AffineTransform at)285     public PathIterator getPathIterator(final AffineTransform at) {
286         return new PathIterator() {
287             int index;
288 
289             // ArcIterator.btan(Math.PI/2)
290             public static final double CtrlVal = 0.5522847498307933;
291             private final double ncv = 1.0 - CtrlVal;
292 
293             // Coordinates of control points for Bezier curves approximating the straight lines
294             // and corners of the rounded rectangle.
295             private final double[][] ctrlpts = {
296                     {0.0, 0.0, 0.0, ulHeight},
297                     {0.0, 0.0, 1.0, -llHeight},
298                     {0.0, 0.0, 1.0, -llHeight * ncv, 0.0, ncv * llWidth, 1.0, 0.0, 0.0, llWidth,
299                             1.0, 0.0},
300                     {1.0, -lrWidth, 1.0, 0.0},
301                     {1.0, -lrWidth * ncv, 1.0, 0.0, 1.0, 0.0, 1.0, -lrHeight * ncv, 1.0, 0.0, 1.0,
302                             -lrHeight},
303                     {1.0, 0.0, 0.0, urHeight},
304                     {1.0, 0.0, 0.0, ncv * urHeight, 1.0, -urWidth * ncv, 0.0, 0.0, 1.0, -urWidth,
305                             0.0, 0.0},
306                     {0.0, ulWidth, 0.0, 0.0},
307                     {0.0, ncv * ulWidth, 0.0, 0.0, 0.0, 0.0, 0.0, ncv * ulHeight, 0.0, 0.0, 0.0,
308                             ulHeight},
309                     {}
310             };
311             private final int[] types = {
312                     SEG_MOVETO,
313                     SEG_LINETO, SEG_CUBICTO,
314                     SEG_LINETO, SEG_CUBICTO,
315                     SEG_LINETO, SEG_CUBICTO,
316                     SEG_LINETO, SEG_CUBICTO,
317                     SEG_CLOSE,
318             };
319 
320             @Override
321             public int getWindingRule() {
322                 return WIND_NON_ZERO;
323             }
324 
325             @Override
326             public boolean isDone() {
327                 return index >= ctrlpts.length;
328             }
329 
330             @Override
331             public void next() {
332                 index++;
333             }
334 
335             @Override
336             public int currentSegment(float[] coords) {
337                 if (isDone()) {
338                     throw new NoSuchElementException("roundrect iterator out of bounds");
339                 }
340                 int nc = 0;
341                 double ctrls[] = ctrlpts[index];
342                 for (int i = 0; i < ctrls.length; i += 4) {
343                     coords[nc++] = (float) (x + ctrls[i] * width + ctrls[i + 1] / 2d);
344                     coords[nc++] = (float) (y + ctrls[i + 2] * height + ctrls[i + 3] / 2d);
345                 }
346                 if (at != null) {
347                     at.transform(coords, 0, coords, 0, nc / 2);
348                 }
349                 return types[index];
350             }
351 
352             @Override
353             public int currentSegment(double[] coords) {
354                 if (isDone()) {
355                     throw new NoSuchElementException("roundrect iterator out of bounds");
356                 }
357                 int nc = 0;
358                 double ctrls[] = ctrlpts[index];
359                 for (int i = 0; i < ctrls.length; i += 4) {
360                     coords[nc++] = x + ctrls[i] * width + ctrls[i + 1] / 2d;
361                     coords[nc++] = y + ctrls[i + 2] * height + ctrls[i + 3] / 2d;
362                 }
363                 if (at != null) {
364                     at.transform(coords, 0, coords, 0, nc / 2);
365                 }
366                 return types[index];
367             }
368         };
369     }
370 }
371