StyledCornersBitmapDrawable.java revision 856e2a9d72fe3a79cd7bfa5cf443d13a60495c4b
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;
28import android.view.View;
29
30import 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 */
43public 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