blob: 558234454c55e67b65620b599b3519f2b7f91b70 [file] [log] [blame]
Mark Weiad6ca3f2014-01-07 00:41:21 -08001/*
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;
Mark Weic5644922014-07-14 16:56:54 -070027import android.util.Log;
Mark Wei856e2a92015-04-14 10:53:05 -070028import android.view.View;
Mark Weiad6ca3f2014-01-07 00:41:21 -080029
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 {
Mark Weic5644922014-07-14 16:56:54 -070044 private static final String TAG = StyledCornersBitmapDrawable.class.getSimpleName();
Mark Weiad6ca3f2014-01-07 00:41:21 -080045
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();
Mark Weidf019662014-06-25 15:40:38 -070059 private final Paint mCompatibilityModeBackgroundPaint = new Paint();
Mark Weiad6ca3f2014-01-07 00:41:21 -080060 private final Path mClipPath = new Path();
Mark Weidf019662014-06-25 15:40:38 -070061 private final Path mCompatibilityModePath = new Path();
Mark Weiad6ca3f2014-01-07 00:41:21 -080062 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;
Mark Wei856e2a92015-04-14 10:53:05 -070069
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
Mark Weiad6ca3f2014-01-07 00:41:21 -080075 private int mScrimColor;
76 private float mBorderWidth;
Mark Weidf019662014-06-25 15:40:38 -070077 private boolean mIsCompatibilityMode;
Mark Weic5644922014-07-14 16:56:54 -070078 private boolean mEatInvalidates;
Mark Weiad6ca3f2014-01-07 00:41:21 -080079
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);
Mark Weidf019662014-06-25 15:40:38 -070093 mFlapPaint.setAntiAlias(true);
Mark Weiad6ca3f2014-01-07 00:41:21 -080094
95 mBorderPaint.setColor(Color.TRANSPARENT);
96 mBorderPaint.setStyle(Style.STROKE);
97 mBorderPaint.setStrokeWidth(mBorderWidth);
98 mBorderPaint.setAntiAlias(true);
99
Mark Weidf019662014-06-25 15:40:38 -0700100 mCompatibilityModeBackgroundPaint.setColor(Color.TRANSPARENT);
101 mCompatibilityModeBackgroundPaint.setStyle(Style.FILL);
102 mCompatibilityModeBackgroundPaint.setAntiAlias(true);
103
Mark Weiad6ca3f2014-01-07 00:41:21 -0800104 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
Mark Wei856e2a92015-04-14 10:53:05 -0700132 /** 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 }
Mark Weiad6ca3f2014-01-07 00:41:21 -0800140
Mark Wei856e2a92015-04-14 10:53:05 -0700141 @Override
142 public void onLayoutDirectionChangeLocal(int layoutDirection) {
143 resolveCornerStyles();
Mark Weiad6ca3f2014-01-07 00:41:21 -0800144 }
145
146 /**
Mark Weidf019662014-06-25 15:40:38 -0700147 * 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 /**
Mark Weiad6ca3f2014-01-07 00:41:21 -0800154 * 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
Mark Weidf019662014-06-25 15:40:38 -0700188 /**
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
Mark Weiad6ca3f2014-01-07 00:41:21 -0800216 @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
Mark Weic5644922014-07-14 16:56:54 -0700234 pauseInvalidate();
235
Mark Weiad6ca3f2014-01-07 00:41:21 -0800236 // Clip to path.
Mark Weidf019662014-06-25 15:40:38 -0700237 if (!mIsCompatibilityMode) {
238 canvas.save();
239 canvas.clipPath(mClipPath);
240 }
Mark Weiad6ca3f2014-01-07 00:41:21 -0800241
242 // Draw parent within path.
243 super.draw(canvas);
244
245 // Draw scrim on top of parent.
246 canvas.drawColor(mScrimColor);
247
Mark Weidf019662014-06-25 15:40:38 -0700248 // Draw flaps.
Mark Weiad6ca3f2014-01-07 00:41:21 -0800249 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
Mark Weidf019662014-06-25 15:40:38 -0700278 if (!mIsCompatibilityMode) {
279 canvas.restore();
280 }
281
282 if (mIsCompatibilityMode) {
283 drawFakeCornersForCompatibilityMode(canvas);
284 }
Mark Weiad6ca3f2014-01-07 00:41:21 -0800285
286 // Draw border around path.
287 canvas.drawPath(mClipPath, mBorderPaint);
Mark Weic5644922014-07-14 16:56:54 -0700288
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 }
Mark Weiad6ca3f2014-01-07 00:41:21 -0800299 }
300
Mark Weidf019662014-06-25 15:40:38 -0700301 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 }
Mark Weiad6ca3f2014-01-07 00:41:21 -0800383 }
384
Mark Weic5644922014-07-14 16:56:54 -0700385 private void pauseInvalidate() {
386 mEatInvalidates = true;
387 }
388
389 private void resumeInvalidate() {
390 mEatInvalidates = false;
391 }
392
Mark Weiad6ca3f2014-01-07 00:41:21 -0800393 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 }
Mark Wei856e2a92015-04-14 10:53:05 -0700469
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 }
Mark Weiad6ca3f2014-01-07 00:41:21 -0800495}