Home | History | Annotate | Download | only in graphics
      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      */
     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
    103     public double getX() {
    104         return x;
    105     }
    106 
    107     @Override
    108     public double getY() {
    109         return y;
    110     }
    111 
    112     @Override
    113     public double getWidth() {
    114         return width;
    115     }
    116 
    117     @Override
    118     public double getHeight() {
    119         return height;
    120     }
    121 
    122     @Override
    123     public boolean isEmpty() {
    124         return (width <= 0d) || (height <= 0d);
    125     }
    126 
    127     @Override
    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
    136     public Rectangle2D getBounds2D() {
    137         return new Rectangle2D.Double(x, y, width, height);
    138     }
    139 
    140     @Override
    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 
    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 
    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 
    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
    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
    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