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