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
17package android.graphics;
18
19import java.awt.geom.AffineTransform;
20import java.awt.geom.PathIterator;
21import java.awt.geom.Rectangle2D;
22import java.awt.geom.RectangularShape;
23import java.awt.geom.RoundRectangle2D;
24import java.util.EnumSet;
25import java.util.NoSuchElementException;
26
27/**
28 * Defines a rectangle with rounded corners, where the sizes of the corners
29 * are potentially different.
30 */
31public 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     */
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
105    public double getX() {
106        return x;
107    }
108
109    @Override
110    public double getY() {
111        return y;
112    }
113
114    @Override
115    public double getWidth() {
116        return width;
117    }
118
119    @Override
120    public double getHeight() {
121        return height;
122    }
123
124    @Override
125    public boolean isEmpty() {
126        return (width <= 0d) || (height <= 0d);
127    }
128
129    @Override
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
138    public Rectangle2D getBounds2D() {
139        return new Rectangle2D.Double(x, y, width, height);
140    }
141
142    @Override
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
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
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
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
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
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