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