StyledCornersBitmapDrawable.java revision df01966f999ddcc69b3e479c9efbd733ad86bc84
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
17package com.android.bitmap.drawable;
18
19import android.content.res.Resources;
20import android.graphics.Canvas;
21import android.graphics.Color;
22import android.graphics.Paint;
23import android.graphics.Paint.Style;
24import android.graphics.Path;
25import android.graphics.Rect;
26import android.graphics.RectF;
27
28import 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 */
41public 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