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