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      */
StyledCornersBitmapDrawable(Resources res, BitmapCache cache, boolean limitDensity, ExtendedOptions opts, float cornerRoundRadius, float cornerFlapSide)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      */
setBorderWidth(final float borderWidth)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      */
setBorderColor(final int color)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 */
setCornerStylesRelative(int topStart, int topEnd, int bottomEnd, int bottomStart)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
onLayoutDirectionChangeLocal(int layoutDirection)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      */
getFlapColor()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      */
setFlapColor(int flapColor)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      */
getScrimColor()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      */
setScrimColor(int color)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      */
setCompatibilityMode(boolean isCompatibilityMode)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      */
setCompatibilityModeBackgroundColor(int color)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
onBoundsChange(Rect bounds)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
draw(Canvas canvas)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
invalidateSelf()293     public void invalidateSelf() {
294         if (!mEatInvalidates) {
295             super.invalidateSelf();
296         } else {
297             Log.d(TAG, "Skipping invalidate.");
298         }
299     }
300 
drawFakeCornersForCompatibilityMode(final Canvas canvas)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 
pauseInvalidate()385     private void pauseInvalidate() {
386         mEatInvalidates = true;
387     }
388 
resumeInvalidate()389     private void resumeInvalidate() {
390         mEatInvalidates = false;
391     }
392 
recalculatePath()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 
resolveCornerStyles()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 */
setCornerStyles(int topLeft, int topRight, int bottomRight, int bottomLeft)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