StyledCornersBitmapDrawable.java revision df01966f999ddcc69b3e479c9efbd733ad86bc84
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; 27 28import com.android.bitmap.BitmapCache; 29 30/** 31 * A custom ExtendedBitmapDrawable that styles the corners in configurable ways. 32 * 33 * All four corners can be configured as {@link #CORNER_STYLE_SHARP}, 34 * {@link #CORNER_STYLE_ROUND}, or {@link #CORNER_STYLE_FLAP}. 35 * This is accomplished applying a non-rectangular clip applied to the canvas. 36 * 37 * A border is draw that conforms to the styled corners. 38 * 39 * {@link #CORNER_STYLE_FLAP} corners have a colored flap drawn within the bounds. 40 */ 41public class StyledCornersBitmapDrawable extends ExtendedBitmapDrawable { 42 43 public static final int CORNER_STYLE_SHARP = 0; 44 public static final int CORNER_STYLE_ROUND = 1; 45 public static final int CORNER_STYLE_FLAP = 2; 46 47 private static final int START_RIGHT = 0; 48 private static final int START_BOTTOM = 90; 49 private static final int START_LEFT = 180; 50 private static final int START_TOP = 270; 51 private static final int QUARTER_CIRCLE = 90; 52 private static final RectF sRectF = new RectF(); 53 54 private final Paint mFlapPaint = new Paint(); 55 private final Paint mBorderPaint = new Paint(); 56 private final Paint mCompatibilityModeBackgroundPaint = new Paint(); 57 private final Path mClipPath = new Path(); 58 private final Path mCompatibilityModePath = new Path(); 59 private final float mCornerRoundRadius; 60 private final float mCornerFlapSide; 61 62 private int mTopLeftCornerStyle = CORNER_STYLE_SHARP; 63 private int mTopRightCornerStyle = CORNER_STYLE_SHARP; 64 private int mBottomRightCornerStyle = CORNER_STYLE_SHARP; 65 private int mBottomLeftCornerStyle = CORNER_STYLE_SHARP; 66 private int mScrimColor; 67 private float mBorderWidth; 68 private boolean mIsCompatibilityMode; 69 70 /** 71 * Create a new StyledCornersBitmapDrawable. 72 */ 73 public StyledCornersBitmapDrawable(Resources res, BitmapCache cache, 74 boolean limitDensity, ExtendedOptions opts, float cornerRoundRadius, 75 float cornerFlapSide) { 76 super(res, cache, limitDensity, opts); 77 78 mCornerRoundRadius = cornerRoundRadius; 79 mCornerFlapSide = cornerFlapSide; 80 81 mFlapPaint.setColor(Color.TRANSPARENT); 82 mFlapPaint.setStyle(Style.FILL); 83 mFlapPaint.setAntiAlias(true); 84 85 mBorderPaint.setColor(Color.TRANSPARENT); 86 mBorderPaint.setStyle(Style.STROKE); 87 mBorderPaint.setStrokeWidth(mBorderWidth); 88 mBorderPaint.setAntiAlias(true); 89 90 mCompatibilityModeBackgroundPaint.setColor(Color.TRANSPARENT); 91 mCompatibilityModeBackgroundPaint.setStyle(Style.FILL); 92 mCompatibilityModeBackgroundPaint.setAntiAlias(true); 93 94 mScrimColor = Color.TRANSPARENT; 95 } 96 97 /** 98 * Set the border stroke width of this drawable. 99 */ 100 public void setBorderWidth(final float borderWidth) { 101 final boolean changed = mBorderPaint.getStrokeWidth() != borderWidth; 102 mBorderPaint.setStrokeWidth(borderWidth); 103 mBorderWidth = borderWidth; 104 105 if (changed) { 106 invalidateSelf(); 107 } 108 } 109 110 /** 111 * Set the border stroke color of this drawable. Set to {@link Color#TRANSPARENT} to disable. 112 */ 113 public void setBorderColor(final int color) { 114 final boolean changed = mBorderPaint.getColor() != color; 115 mBorderPaint.setColor(color); 116 117 if (changed) { 118 invalidateSelf(); 119 } 120 } 121 122 /** Set the corner styles for all four corners */ 123 public void setCornerStyles(int topLeft, int topRight, int bottomRight, int bottomLeft) { 124 boolean changed = mTopLeftCornerStyle != topLeft 125 || mTopRightCornerStyle != topRight 126 || mBottomRightCornerStyle != bottomRight 127 || mBottomLeftCornerStyle != bottomLeft; 128 129 mTopLeftCornerStyle = topLeft; 130 mTopRightCornerStyle = topRight; 131 mBottomRightCornerStyle = bottomRight; 132 mBottomLeftCornerStyle = bottomLeft; 133 134 if (changed) { 135 recalculatePath(); 136 } 137 } 138 139 /** 140 * Get the flap color for all corners that have style {@link #CORNER_STYLE_SHARP}. 141 */ 142 public int getFlapColor() { 143 return mFlapPaint.getColor(); 144 } 145 146 /** 147 * Set the flap color for all corners that have style {@link #CORNER_STYLE_SHARP}. 148 * 149 * Use {@link android.graphics.Color#TRANSPARENT} to disable flap colors. 150 */ 151 public void setFlapColor(int flapColor) { 152 boolean changed = mFlapPaint.getColor() != flapColor; 153 mFlapPaint.setColor(flapColor); 154 155 if (changed) { 156 invalidateSelf(); 157 } 158 } 159 160 /** 161 * Get the color of the scrim that is drawn over the contents, but under the flaps and borders. 162 */ 163 public int getScrimColor() { 164 return mScrimColor; 165 } 166 167 /** 168 * Set the color of the scrim that is drawn over the contents, but under the flaps and borders. 169 * 170 * Use {@link android.graphics.Color#TRANSPARENT} to disable the scrim. 171 */ 172 public void setScrimColor(int color) { 173 boolean changed = mScrimColor != color; 174 mScrimColor = color; 175 176 if (changed) { 177 invalidateSelf(); 178 } 179 } 180 181 /** 182 * Sets whether we should work around an issue introduced in Android 4.4.3, 183 * where a WebView can corrupt the stencil buffer of the canvas when the canvas is clipped 184 * using a non-rectangular Path. 185 */ 186 public void setCompatibilityMode(boolean isCompatibilityMode) { 187 boolean changed = mIsCompatibilityMode != isCompatibilityMode; 188 mIsCompatibilityMode = isCompatibilityMode; 189 190 if (changed) { 191 invalidateSelf(); 192 } 193 } 194 195 /** 196 * Sets the color of the container that this drawable is in. The given color will be used in 197 * {@link #setCompatibilityMode compatibility mode} to draw fake corners to emulate clipped 198 * corners. 199 */ 200 public void setCompatibilityModeBackgroundColor(int color) { 201 boolean changed = mCompatibilityModeBackgroundPaint.getColor() != color; 202 mCompatibilityModeBackgroundPaint.setColor(color); 203 204 if (changed) { 205 invalidateSelf(); 206 } 207 } 208 209 @Override 210 protected void onBoundsChange(Rect bounds) { 211 super.onBoundsChange(bounds); 212 213 recalculatePath(); 214 } 215 216 /** 217 * Override draw(android.graphics.Canvas) instead of 218 * {@link #onDraw(android.graphics.Canvas)} to clip all the drawable layers. 219 */ 220 @Override 221 public void draw(Canvas canvas) { 222 final Rect bounds = getBounds(); 223 if (bounds.isEmpty()) { 224 return; 225 } 226 227 // Clip to path. 228 if (!mIsCompatibilityMode) { 229 canvas.save(); 230 canvas.clipPath(mClipPath); 231 } 232 233 // Draw parent within path. 234 super.draw(canvas); 235 236 // Draw scrim on top of parent. 237 canvas.drawColor(mScrimColor); 238 239 // Draw flaps. 240 float left = bounds.left + mBorderWidth / 2; 241 float top = bounds.top + mBorderWidth / 2; 242 float right = bounds.right - mBorderWidth / 2; 243 float bottom = bounds.bottom - mBorderWidth / 2; 244 RectF flapCornerRectF = sRectF; 245 flapCornerRectF.set(0, 0, mCornerFlapSide + mCornerRoundRadius, 246 mCornerFlapSide + mCornerRoundRadius); 247 248 if (mTopLeftCornerStyle == CORNER_STYLE_FLAP) { 249 flapCornerRectF.offsetTo(left, top); 250 canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius, 251 mCornerRoundRadius, mFlapPaint); 252 } 253 if (mTopRightCornerStyle == CORNER_STYLE_FLAP) { 254 flapCornerRectF.offsetTo(right - mCornerFlapSide, top); 255 canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius, 256 mCornerRoundRadius, mFlapPaint); 257 } 258 if (mBottomRightCornerStyle == CORNER_STYLE_FLAP) { 259 flapCornerRectF.offsetTo(right - mCornerFlapSide, bottom - mCornerFlapSide); 260 canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius, 261 mCornerRoundRadius, mFlapPaint); 262 } 263 if (mBottomLeftCornerStyle == CORNER_STYLE_FLAP) { 264 flapCornerRectF.offsetTo(left, bottom - mCornerFlapSide); 265 canvas.drawRoundRect(flapCornerRectF, mCornerRoundRadius, 266 mCornerRoundRadius, mFlapPaint); 267 } 268 269 if (!mIsCompatibilityMode) { 270 canvas.restore(); 271 } 272 273 if (mIsCompatibilityMode) { 274 drawFakeCornersForCompatibilityMode(canvas); 275 } 276 277 // Draw border around path. 278 canvas.drawPath(mClipPath, mBorderPaint); 279 } 280 281 protected void drawFakeCornersForCompatibilityMode(final Canvas canvas) { 282 final Rect bounds = getBounds(); 283 284 float left = bounds.left; 285 float top = bounds.top; 286 float right = bounds.right; 287 float bottom = bounds.bottom; 288 289 // Draw fake round corners. 290 RectF fakeCornerRectF = sRectF; 291 fakeCornerRectF.set(0, 0, mCornerRoundRadius * 2, mCornerRoundRadius * 2); 292 if (mTopLeftCornerStyle == CORNER_STYLE_ROUND) { 293 fakeCornerRectF.offsetTo(left, top); 294 mCompatibilityModePath.rewind(); 295 mCompatibilityModePath.moveTo(left, top); 296 mCompatibilityModePath.lineTo(left + mCornerRoundRadius, top); 297 mCompatibilityModePath.arcTo(fakeCornerRectF, START_TOP, -QUARTER_CIRCLE); 298 mCompatibilityModePath.close(); 299 canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); 300 } 301 if (mTopRightCornerStyle == CORNER_STYLE_ROUND) { 302 fakeCornerRectF.offsetTo(right - fakeCornerRectF.width(), top); 303 mCompatibilityModePath.rewind(); 304 mCompatibilityModePath.moveTo(right, top); 305 mCompatibilityModePath.lineTo(right, top + mCornerRoundRadius); 306 mCompatibilityModePath.arcTo(fakeCornerRectF, START_RIGHT, -QUARTER_CIRCLE); 307 mCompatibilityModePath.close(); 308 canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); 309 } 310 if (mBottomRightCornerStyle == CORNER_STYLE_ROUND) { 311 fakeCornerRectF 312 .offsetTo(right - fakeCornerRectF.width(), bottom - fakeCornerRectF.height()); 313 mCompatibilityModePath.rewind(); 314 mCompatibilityModePath.moveTo(right, bottom); 315 mCompatibilityModePath.lineTo(right - mCornerRoundRadius, bottom); 316 mCompatibilityModePath.arcTo(fakeCornerRectF, START_BOTTOM, -QUARTER_CIRCLE); 317 mCompatibilityModePath.close(); 318 canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); 319 } 320 if (mBottomLeftCornerStyle == CORNER_STYLE_ROUND) { 321 fakeCornerRectF.offsetTo(left, bottom - fakeCornerRectF.height()); 322 mCompatibilityModePath.rewind(); 323 mCompatibilityModePath.moveTo(left, bottom); 324 mCompatibilityModePath.lineTo(left, bottom - mCornerRoundRadius); 325 mCompatibilityModePath.arcTo(fakeCornerRectF, START_LEFT, -QUARTER_CIRCLE); 326 mCompatibilityModePath.close(); 327 canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); 328 } 329 330 // Draw fake flap corners. 331 if (mTopLeftCornerStyle == CORNER_STYLE_FLAP) { 332 mCompatibilityModePath.rewind(); 333 mCompatibilityModePath.moveTo(left, top); 334 mCompatibilityModePath.lineTo(left + mCornerFlapSide, top); 335 mCompatibilityModePath.lineTo(left, top + mCornerFlapSide); 336 mCompatibilityModePath.close(); 337 canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); 338 } 339 if (mTopRightCornerStyle == CORNER_STYLE_FLAP) { 340 mCompatibilityModePath.rewind(); 341 mCompatibilityModePath.moveTo(right, top); 342 mCompatibilityModePath.lineTo(right, top + mCornerFlapSide); 343 mCompatibilityModePath.lineTo(right - mCornerFlapSide, top); 344 mCompatibilityModePath.close(); 345 canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); 346 } 347 if (mBottomRightCornerStyle == CORNER_STYLE_FLAP) { 348 mCompatibilityModePath.rewind(); 349 mCompatibilityModePath.moveTo(right, bottom); 350 mCompatibilityModePath.lineTo(right - mCornerFlapSide, bottom); 351 mCompatibilityModePath.lineTo(right, bottom - mCornerFlapSide); 352 mCompatibilityModePath.close(); 353 canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); 354 } 355 if (mBottomLeftCornerStyle == CORNER_STYLE_FLAP) { 356 mCompatibilityModePath.rewind(); 357 mCompatibilityModePath.moveTo(left, bottom); 358 mCompatibilityModePath.lineTo(left, bottom - mCornerFlapSide); 359 mCompatibilityModePath.lineTo(left + mCornerFlapSide, bottom); 360 mCompatibilityModePath.close(); 361 canvas.drawPath(mCompatibilityModePath, mCompatibilityModeBackgroundPaint); 362 } 363 } 364 365 private void recalculatePath() { 366 Rect bounds = getBounds(); 367 368 if (bounds.isEmpty()) { 369 return; 370 } 371 372 // Setup. 373 float left = bounds.left + mBorderWidth / 2; 374 float top = bounds.top + mBorderWidth / 2; 375 float right = bounds.right - mBorderWidth / 2; 376 float bottom = bounds.bottom - mBorderWidth / 2; 377 RectF roundedCornerRectF = sRectF; 378 roundedCornerRectF.set(0, 0, 2 * mCornerRoundRadius, 2 * mCornerRoundRadius); 379 mClipPath.rewind(); 380 381 switch (mTopLeftCornerStyle) { 382 case CORNER_STYLE_SHARP: 383 mClipPath.moveTo(left, top); 384 break; 385 case CORNER_STYLE_ROUND: 386 roundedCornerRectF.offsetTo(left, top); 387 mClipPath.arcTo(roundedCornerRectF, START_LEFT, QUARTER_CIRCLE); 388 break; 389 case CORNER_STYLE_FLAP: 390 mClipPath.moveTo(left, top - mCornerFlapSide); 391 mClipPath.lineTo(left + mCornerFlapSide, top); 392 break; 393 } 394 395 switch (mTopRightCornerStyle) { 396 case CORNER_STYLE_SHARP: 397 mClipPath.lineTo(right, top); 398 break; 399 case CORNER_STYLE_ROUND: 400 roundedCornerRectF.offsetTo(right - roundedCornerRectF.width(), top); 401 mClipPath.arcTo(roundedCornerRectF, START_TOP, QUARTER_CIRCLE); 402 break; 403 case CORNER_STYLE_FLAP: 404 mClipPath.lineTo(right - mCornerFlapSide, top); 405 mClipPath.lineTo(right, top + mCornerFlapSide); 406 break; 407 } 408 409 switch (mBottomRightCornerStyle) { 410 case CORNER_STYLE_SHARP: 411 mClipPath.lineTo(right, bottom); 412 break; 413 case CORNER_STYLE_ROUND: 414 roundedCornerRectF.offsetTo(right - roundedCornerRectF.width(), 415 bottom - roundedCornerRectF.height()); 416 mClipPath.arcTo(roundedCornerRectF, START_RIGHT, QUARTER_CIRCLE); 417 break; 418 case CORNER_STYLE_FLAP: 419 mClipPath.lineTo(right, bottom - mCornerFlapSide); 420 mClipPath.lineTo(right - mCornerFlapSide, bottom); 421 break; 422 } 423 424 switch (mBottomLeftCornerStyle) { 425 case CORNER_STYLE_SHARP: 426 mClipPath.lineTo(left, bottom); 427 break; 428 case CORNER_STYLE_ROUND: 429 roundedCornerRectF.offsetTo(left, bottom - roundedCornerRectF.height()); 430 mClipPath.arcTo(roundedCornerRectF, START_BOTTOM, QUARTER_CIRCLE); 431 break; 432 case CORNER_STYLE_FLAP: 433 mClipPath.lineTo(left + mCornerFlapSide, bottom); 434 mClipPath.lineTo(left, bottom - mCornerFlapSide); 435 break; 436 } 437 438 // Finish. 439 mClipPath.close(); 440 } 441} 442