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