Home | History | Annotate | Download | only in drawable
      1 /*
      2  * Copyright (C) 2014 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 com.android.bitmap.drawable;
     18 
     19 import android.content.res.Resources;
     20 import android.graphics.Canvas;
     21 import android.graphics.Color;
     22 import android.graphics.Paint;
     23 import android.graphics.Paint.Style;
     24 import android.graphics.Path;
     25 import android.graphics.Rect;
     26 import android.graphics.RectF;
     27 
     28 import com.android.bitmap.BitmapCache;
     29 
     30 /**
     31  * A custom ExtendedBitmapDrawable that styles the corners in configurable ways.
     32  *
     33  * All four corners can be configured as {@link #CORNER_STYLE_SHARP},
     34  * {@link #CORNER_STYLE_ROUND}, or {@link #CORNER_STYLE_FLAP}.
     35  * This is accomplished applying a non-rectangular clip applied to the canvas.
     36  *
     37  * A border is draw that conforms to the styled corners.
     38  *
     39  * {@link #CORNER_STYLE_FLAP} corners have a colored flap drawn within the bounds.
     40  */
     41 public class StyledCornersBitmapDrawable extends ExtendedBitmapDrawable {
     42 
     43     public static final int CORNER_STYLE_SHARP = 0;
     44     public static final int CORNER_STYLE_ROUND = 1;
     45     public static final int CORNER_STYLE_FLAP = 2;
     46 
     47     private static final int START_RIGHT = 0;
     48     private static final int START_BOTTOM = 90;
     49     private static final int START_LEFT = 180;
     50     private static final int START_TOP = 270;
     51     private static final int QUARTER_CIRCLE = 90;
     52     private static final RectF sRectF = new RectF();
     53 
     54     private final Paint mFlapPaint = new Paint();
     55     private final Paint mBorderPaint = new Paint();
     56     private final Paint mCompatibilityModeBackgroundPaint = new Paint();
     57     private final Path mClipPath = new Path();
     58     private final Path mCompatibilityModePath = new Path();
     59     private final float mCornerRoundRadius;
     60     private final float mCornerFlapSide;
     61 
     62     private int mTopLeftCornerStyle = CORNER_STYLE_SHARP;
     63     private int mTopRightCornerStyle = CORNER_STYLE_SHARP;
     64     private int mBottomRightCornerStyle = CORNER_STYLE_SHARP;
     65     private int mBottomLeftCornerStyle = CORNER_STYLE_SHARP;
     66     private int mScrimColor;
     67     private float mBorderWidth;
     68     private boolean mIsCompatibilityMode;
     69 
     70     /**
     71      * Create a new StyledCornersBitmapDrawable.
     72      */
     73     public StyledCornersBitmapDrawable(Resources res, BitmapCache cache,
     74             boolean limitDensity, ExtendedOptions opts, float cornerRoundRadius,
     75             float cornerFlapSide) {
     76         super(res, cache, limitDensity, opts);
     77 
     78         mCornerRoundRadius = cornerRoundRadius;
     79         mCornerFlapSide = cornerFlapSide;
     80 
     81         mFlapPaint.setColor(Color.TRANSPARENT);
     82         mFlapPaint.setStyle(Style.FILL);
     83         mFlapPaint.setAntiAlias(true);
     84 
     85         mBorderPaint.setColor(Color.TRANSPARENT);
     86         mBorderPaint.setStyle(Style.STROKE);
     87         mBorderPaint.setStrokeWidth(mBorderWidth);
     88         mBorderPaint.setAntiAlias(true);
     89 
     90         mCompatibilityModeBackgroundPaint.setColor(Color.TRANSPARENT);
     91         mCompatibilityModeBackgroundPaint.setStyle(Style.FILL);
     92         mCompatibilityModeBackgroundPaint.setAntiAlias(true);
     93 
     94         mScrimColor = Color.TRANSPARENT;
     95     }
     96 
     97     /**
     98      * Set the border stroke width of this drawable.
     99      */
    100     public void setBorderWidth(final float borderWidth) {
    101         final boolean changed = mBorderPaint.getStrokeWidth() != borderWidth;
    102         mBorderPaint.setStrokeWidth(borderWidth);
    103         mBorderWidth = borderWidth;
    104 
    105         if (changed) {
    106             invalidateSelf();
    107         }
    108     }
    109 
    110     /**
    111      * Set the border stroke color of this drawable. Set to {@link Color#TRANSPARENT} to disable.
    112      */
    113     public void setBorderColor(final int color) {
    114         final boolean changed = mBorderPaint.getColor() != color;
    115         mBorderPaint.setColor(color);
    116 
    117         if (changed) {
    118             invalidateSelf();
    119         }
    120     }
    121 
    122     /** Set the corner styles for all four corners */
    123     public void setCornerStyles(int topLeft, int topRight, int bottomRight, int bottomLeft) {
    124         boolean changed = mTopLeftCornerStyle != topLeft
    125                 || mTopRightCornerStyle != topRight
    126                 || mBottomRightCornerStyle != bottomRight
    127                 || mBottomLeftCornerStyle != bottomLeft;
    128 
    129         mTopLeftCornerStyle = topLeft;
    130         mTopRightCornerStyle = topRight;
    131         mBottomRightCornerStyle = bottomRight;
    132         mBottomLeftCornerStyle = bottomLeft;
    133 
    134         if (changed) {
    135             recalculatePath();
    136         }
    137     }
    138 
    139     /**
    140      * Get the flap color for all corners that have style {@link #CORNER_STYLE_SHARP}.
    141      */
    142     public int getFlapColor() {
    143         return mFlapPaint.getColor();
    144     }
    145 
    146     /**
    147      * Set the flap color for all corners that have style {@link #CORNER_STYLE_SHARP}.
    148      *
    149      * Use {@link android.graphics.Color#TRANSPARENT} to disable flap colors.
    150      */
    151     public void setFlapColor(int flapColor) {
    152         boolean changed = mFlapPaint.getColor() != flapColor;
    153         mFlapPaint.setColor(flapColor);
    154 
    155         if (changed) {
    156             invalidateSelf();
    157         }
    158     }
    159 
    160     /**
    161      * Get the color of the scrim that is drawn over the contents, but under the flaps and borders.
    162      */
    163     public int getScrimColor() {
    164         return mScrimColor;
    165     }
    166 
    167     /**
    168      * Set the color of the scrim that is drawn over the contents, but under the flaps and borders.
    169      *
    170      * Use {@link android.graphics.Color#TRANSPARENT} to disable the scrim.
    171      */
    172     public void setScrimColor(int color) {
    173         boolean changed = mScrimColor != color;
    174         mScrimColor = color;
    175 
    176         if (changed) {
    177             invalidateSelf();
    178         }
    179     }
    180 
    181     /**
    182      * Sets whether we should work around an issue introduced in Android 4.4.3,
    183      * where a WebView can corrupt the stencil buffer of the canvas when the canvas is clipped
    184      * using a non-rectangular Path.
    185      */
    186     public void setCompatibilityMode(boolean isCompatibilityMode) {
    187         boolean changed = mIsCompatibilityMode != isCompatibilityMode;
    188         mIsCompatibilityMode = isCompatibilityMode;
    189 
    190         if (changed) {
    191             invalidateSelf();
    192         }
    193     }
    194 
    195     /**
    196      * Sets the color of the container that this drawable is in. The given color will be used in
    197      * {@link #setCompatibilityMode compatibility mode} to draw fake corners to emulate clipped
    198      * corners.
    199      */
    200     public void setCompatibilityModeBackgroundColor(int color) {
    201         boolean changed = mCompatibilityModeBackgroundPaint.getColor() != color;
    202         mCompatibilityModeBackgroundPaint.setColor(color);
    203 
    204         if (changed) {
    205             invalidateSelf();
    206         }
    207     }
    208 
    209     @Override
    210     protected void onBoundsChange(Rect bounds) {
    211         super.onBoundsChange(bounds);
    212 
    213         recalculatePath();
    214     }
    215 
    216     /**
    217      * Override draw(android.graphics.Canvas) instead of
    218      * {@link #onDraw(android.graphics.Canvas)} to clip all the drawable layers.
    219      */
    220     @Override
    221     public void draw(Canvas canvas) {
    222         final Rect bounds = getBounds();
    223         if (bounds.isEmpty()) {
    224             return;
    225         }
    226 
    227         // Clip to path.
    228         if (!mIsCompatibilityMode) {
    229             canvas.save();
    230             canvas.clipPath(mClipPath);
    231         }
    232 
    233         // Draw parent within path.
    234         super.draw(canvas);
    235 
    236         // Draw scrim on top of parent.
    237         canvas.drawColor(mScrimColor);
    238 
    239         // Draw flaps.
    240         float left = bounds.left + mBorderWidth / 2;
    241         float top = bounds.top + mBorderWidth / 2;
    242         float right = bounds.right - mBorderWidth / 2;
    243         float bottom = bounds.bottom - mBorderWidth / 2;
    244         RectF flapCornerRectF = sRectF;
    245         flapCornerRectF.set(0, 0, mCornerFlapSide + mCornerRoundRadius,
    246                 mCornerFlapSide + mCornerRoundRadius);
    247 
    248         if (mTopLeftCornerStyle == CORNER_STYLE_FLAP) {
    249             flapCornerRectF.offsetTo(left, top);
    250             canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius,
    251                     mCornerRoundRadius, mFlapPaint);
    252         }
    253         if (mTopRightCornerStyle == CORNER_STYLE_FLAP) {
    254             flapCornerRectF.offsetTo(right - mCornerFlapSide, top);
    255             canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius,
    256                     mCornerRoundRadius, mFlapPaint);
    257         }
    258         if (mBottomRightCornerStyle == CORNER_STYLE_FLAP) {
    259             flapCornerRectF.offsetTo(right - mCornerFlapSide, bottom - mCornerFlapSide);
    260             canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius,
    261                     mCornerRoundRadius, mFlapPaint);
    262         }
    263         if (mBottomLeftCornerStyle == CORNER_STYLE_FLAP) {
    264             flapCornerRectF.offsetTo(left, bottom - mCornerFlapSide);
    265             canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius,
    266                     mCornerRoundRadius, mFlapPaint);
    267         }
    268 
    269         if (!mIsCompatibilityMode) {
    270             canvas.restore();
    271         }
    272 
    273         if (mIsCompatibilityMode) {
    274             drawFakeCornersForCompatibilityMode(canvas);
    275         }
    276 
    277         // Draw border around path.
    278         canvas.drawPath(mClipPath, mBorderPaint);
    279     }
    280 
    281     protected void drawFakeCornersForCompatibilityMode(final Canvas canvas) {
    282         final Rect bounds = getBounds();
    283 
    284         float left = bounds.left;
    285         float top = bounds.top;
    286         float right = bounds.right;
    287         float bottom = bounds.bottom;
    288 
    289         // Draw fake round corners.
    290         RectF fakeCornerRectF = sRectF;
    291         fakeCornerRectF.set(0, 0, mCornerRoundRadius * 2, mCornerRoundRadius * 2);
    292         if (mTopLeftCornerStyle == CORNER_STYLE_ROUND) {
    293             fakeCornerRectF.offsetTo(left, top);
    294             mCompatibilityModePath.rewind();
    295             mCompatibilityModePath.moveTo(left, top);
    296             mCompatibilityModePath.lineTo(left + mCornerRoundRadius, top);
    297             mCompatibilityModePath.arcTo(fakeCornerRectF, START_TOP, -QUARTER_CIRCLE);
    298             mCompatibilityModePath.close();
    299             canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint);
    300         }
    301         if (mTopRightCornerStyle == CORNER_STYLE_ROUND) {
    302             fakeCornerRectF.offsetTo(right - fakeCornerRectF.width(), top);
    303             mCompatibilityModePath.rewind();
    304             mCompatibilityModePath.moveTo(right, top);
    305             mCompatibilityModePath.lineTo(right, top + mCornerRoundRadius);
    306             mCompatibilityModePath.arcTo(fakeCornerRectF, START_RIGHT, -QUARTER_CIRCLE);
    307             mCompatibilityModePath.close();
    308             canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint);
    309         }
    310         if (mBottomRightCornerStyle == CORNER_STYLE_ROUND) {
    311             fakeCornerRectF
    312                     .offsetTo(right - fakeCornerRectF.width(), bottom - fakeCornerRectF.height());
    313             mCompatibilityModePath.rewind();
    314             mCompatibilityModePath.moveTo(right, bottom);
    315             mCompatibilityModePath.lineTo(right - mCornerRoundRadius, bottom);
    316             mCompatibilityModePath.arcTo(fakeCornerRectF, START_BOTTOM, -QUARTER_CIRCLE);
    317             mCompatibilityModePath.close();
    318             canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint);
    319         }
    320         if (mBottomLeftCornerStyle == CORNER_STYLE_ROUND) {
    321             fakeCornerRectF.offsetTo(left, bottom - fakeCornerRectF.height());
    322             mCompatibilityModePath.rewind();
    323             mCompatibilityModePath.moveTo(left, bottom);
    324             mCompatibilityModePath.lineTo(left, bottom - mCornerRoundRadius);
    325             mCompatibilityModePath.arcTo(fakeCornerRectF, START_LEFT, -QUARTER_CIRCLE);
    326             mCompatibilityModePath.close();
    327             canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint);
    328         }
    329 
    330         // Draw fake flap corners.
    331         if (mTopLeftCornerStyle == CORNER_STYLE_FLAP) {
    332             mCompatibilityModePath.rewind();
    333             mCompatibilityModePath.moveTo(left, top);
    334             mCompatibilityModePath.lineTo(left + mCornerFlapSide, top);
    335             mCompatibilityModePath.lineTo(left, top + mCornerFlapSide);
    336             mCompatibilityModePath.close();
    337             canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint);
    338         }
    339         if (mTopRightCornerStyle == CORNER_STYLE_FLAP) {
    340             mCompatibilityModePath.rewind();
    341             mCompatibilityModePath.moveTo(right, top);
    342             mCompatibilityModePath.lineTo(right, top + mCornerFlapSide);
    343             mCompatibilityModePath.lineTo(right - mCornerFlapSide, top);
    344             mCompatibilityModePath.close();
    345             canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint);
    346         }
    347         if (mBottomRightCornerStyle == CORNER_STYLE_FLAP) {
    348             mCompatibilityModePath.rewind();
    349             mCompatibilityModePath.moveTo(right, bottom);
    350             mCompatibilityModePath.lineTo(right - mCornerFlapSide, bottom);
    351             mCompatibilityModePath.lineTo(right, bottom - mCornerFlapSide);
    352             mCompatibilityModePath.close();
    353             canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint);
    354         }
    355         if (mBottomLeftCornerStyle == CORNER_STYLE_FLAP) {
    356             mCompatibilityModePath.rewind();
    357             mCompatibilityModePath.moveTo(left, bottom);
    358             mCompatibilityModePath.lineTo(left, bottom - mCornerFlapSide);
    359             mCompatibilityModePath.lineTo(left + mCornerFlapSide, bottom);
    360             mCompatibilityModePath.close();
    361             canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint);
    362         }
    363     }
    364 
    365     private void recalculatePath() {
    366         Rect bounds = getBounds();
    367 
    368         if (bounds.isEmpty()) {
    369             return;
    370         }
    371 
    372         // Setup.
    373         float left = bounds.left + mBorderWidth / 2;
    374         float top = bounds.top + mBorderWidth / 2;
    375         float right = bounds.right - mBorderWidth / 2;
    376         float bottom = bounds.bottom - mBorderWidth / 2;
    377         RectF roundedCornerRectF = sRectF;
    378         roundedCornerRectF.set(0, 0, 2 * mCornerRoundRadius, 2 * mCornerRoundRadius);
    379         mClipPath.rewind();
    380 
    381         switch (mTopLeftCornerStyle) {
    382             case CORNER_STYLE_SHARP:
    383                 mClipPath.moveTo(left, top);
    384                 break;
    385             case CORNER_STYLE_ROUND:
    386                 roundedCornerRectF.offsetTo(left, top);
    387                 mClipPath.arcTo(roundedCornerRectF, START_LEFT, QUARTER_CIRCLE);
    388                 break;
    389             case CORNER_STYLE_FLAP:
    390                 mClipPath.moveTo(left, top - mCornerFlapSide);
    391                 mClipPath.lineTo(left + mCornerFlapSide, top);
    392                 break;
    393         }
    394 
    395         switch (mTopRightCornerStyle) {
    396             case CORNER_STYLE_SHARP:
    397                 mClipPath.lineTo(right, top);
    398                 break;
    399             case CORNER_STYLE_ROUND:
    400                 roundedCornerRectF.offsetTo(right - roundedCornerRectF.width(), top);
    401                 mClipPath.arcTo(roundedCornerRectF, START_TOP, QUARTER_CIRCLE);
    402                 break;
    403             case CORNER_STYLE_FLAP:
    404                 mClipPath.lineTo(right - mCornerFlapSide, top);
    405                 mClipPath.lineTo(right, top + mCornerFlapSide);
    406                 break;
    407         }
    408 
    409         switch (mBottomRightCornerStyle) {
    410             case CORNER_STYLE_SHARP:
    411                 mClipPath.lineTo(right, bottom);
    412                 break;
    413             case CORNER_STYLE_ROUND:
    414                 roundedCornerRectF.offsetTo(right - roundedCornerRectF.width(),
    415                         bottom - roundedCornerRectF.height());
    416                 mClipPath.arcTo(roundedCornerRectF, START_RIGHT, QUARTER_CIRCLE);
    417                 break;
    418             case CORNER_STYLE_FLAP:
    419                 mClipPath.lineTo(right, bottom - mCornerFlapSide);
    420                 mClipPath.lineTo(right - mCornerFlapSide, bottom);
    421                 break;
    422         }
    423 
    424         switch (mBottomLeftCornerStyle) {
    425             case CORNER_STYLE_SHARP:
    426                 mClipPath.lineTo(left, bottom);
    427                 break;
    428             case CORNER_STYLE_ROUND:
    429                 roundedCornerRectF.offsetTo(left, bottom - roundedCornerRectF.height());
    430                 mClipPath.arcTo(roundedCornerRectF, START_BOTTOM, QUARTER_CIRCLE);
    431                 break;
    432             case CORNER_STYLE_FLAP:
    433                 mClipPath.lineTo(left + mCornerFlapSide, bottom);
    434                 mClipPath.lineTo(left, bottom - mCornerFlapSide);
    435                 break;
    436         }
    437 
    438         // Finish.
    439         mClipPath.close();
    440     }
    441 }
    442