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 */ 16package android.support.v7.widget; 17 18import android.content.res.ColorStateList; 19import android.content.res.Resources; 20import android.graphics.Canvas; 21import android.graphics.Color; 22import android.graphics.ColorFilter; 23import android.graphics.LinearGradient; 24import android.graphics.Paint; 25import android.graphics.Path; 26import android.graphics.PixelFormat; 27import android.graphics.RadialGradient; 28import android.graphics.Rect; 29import android.graphics.RectF; 30import android.graphics.Shader; 31import android.graphics.drawable.Drawable; 32import android.support.annotation.Nullable; 33import android.support.v7.cardview.R; 34 35/** 36 * A rounded rectangle drawable which also includes a shadow around. 37 */ 38class RoundRectDrawableWithShadow extends Drawable { 39 // used to calculate content padding 40 private static final double COS_45 = Math.cos(Math.toRadians(45)); 41 42 private static final float SHADOW_MULTIPLIER = 1.5f; 43 44 private final int mInsetShadow; // extra shadow to avoid gaps between card and shadow 45 46 /* 47 * This helper is set by CardView implementations. 48 * <p> 49 * Prior to API 17, canvas.drawRoundRect is expensive; which is why we need this interface 50 * to draw efficient rounded rectangles before 17. 51 * */ 52 static RoundRectHelper sRoundRectHelper; 53 54 private Paint mPaint; 55 56 private Paint mCornerShadowPaint; 57 58 private Paint mEdgeShadowPaint; 59 60 private final RectF mCardBounds; 61 62 private float mCornerRadius; 63 64 private Path mCornerShadowPath; 65 66 // actual value set by developer 67 private float mRawMaxShadowSize; 68 69 // multiplied value to account for shadow offset 70 private float mShadowSize; 71 72 // actual value set by developer 73 private float mRawShadowSize; 74 75 private ColorStateList mBackground; 76 77 private boolean mDirty = true; 78 79 private final int mShadowStartColor; 80 81 private final int mShadowEndColor; 82 83 private boolean mAddPaddingForCorners = true; 84 85 /** 86 * If shadow size is set to a value above max shadow, we print a warning 87 */ 88 private boolean mPrintedShadowClipWarning = false; 89 90 RoundRectDrawableWithShadow(Resources resources, ColorStateList backgroundColor, float radius, 91 float shadowSize, float maxShadowSize) { 92 mShadowStartColor = resources.getColor(R.color.cardview_shadow_start_color); 93 mShadowEndColor = resources.getColor(R.color.cardview_shadow_end_color); 94 mInsetShadow = resources.getDimensionPixelSize(R.dimen.cardview_compat_inset_shadow); 95 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); 96 setBackground(backgroundColor); 97 mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); 98 mCornerShadowPaint.setStyle(Paint.Style.FILL); 99 mCornerRadius = (int) (radius + .5f); 100 mCardBounds = new RectF(); 101 mEdgeShadowPaint = new Paint(mCornerShadowPaint); 102 mEdgeShadowPaint.setAntiAlias(false); 103 setShadowSize(shadowSize, maxShadowSize); 104 } 105 106 private void setBackground(ColorStateList color) { 107 mBackground = (color == null) ? ColorStateList.valueOf(Color.TRANSPARENT) : color; 108 mPaint.setColor(mBackground.getColorForState(getState(), mBackground.getDefaultColor())); 109 } 110 111 /** 112 * Casts the value to an even integer. 113 */ 114 private int toEven(float value) { 115 int i = (int) (value + .5f); 116 if (i % 2 == 1) { 117 return i - 1; 118 } 119 return i; 120 } 121 122 void setAddPaddingForCorners(boolean addPaddingForCorners) { 123 mAddPaddingForCorners = addPaddingForCorners; 124 invalidateSelf(); 125 } 126 127 @Override 128 public void setAlpha(int alpha) { 129 mPaint.setAlpha(alpha); 130 mCornerShadowPaint.setAlpha(alpha); 131 mEdgeShadowPaint.setAlpha(alpha); 132 } 133 134 @Override 135 protected void onBoundsChange(Rect bounds) { 136 super.onBoundsChange(bounds); 137 mDirty = true; 138 } 139 140 private void setShadowSize(float shadowSize, float maxShadowSize) { 141 if (shadowSize < 0f) { 142 throw new IllegalArgumentException("Invalid shadow size " + shadowSize 143 + ". Must be >= 0"); 144 } 145 if (maxShadowSize < 0f) { 146 throw new IllegalArgumentException("Invalid max shadow size " + maxShadowSize 147 + ". Must be >= 0"); 148 } 149 shadowSize = toEven(shadowSize); 150 maxShadowSize = toEven(maxShadowSize); 151 if (shadowSize > maxShadowSize) { 152 shadowSize = maxShadowSize; 153 if (!mPrintedShadowClipWarning) { 154 mPrintedShadowClipWarning = true; 155 } 156 } 157 if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) { 158 return; 159 } 160 mRawShadowSize = shadowSize; 161 mRawMaxShadowSize = maxShadowSize; 162 mShadowSize = (int) (shadowSize * SHADOW_MULTIPLIER + mInsetShadow + .5f); 163 mDirty = true; 164 invalidateSelf(); 165 } 166 167 @Override 168 public boolean getPadding(Rect padding) { 169 int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius, 170 mAddPaddingForCorners)); 171 int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius, 172 mAddPaddingForCorners)); 173 padding.set(hOffset, vOffset, hOffset, vOffset); 174 return true; 175 } 176 177 static float calculateVerticalPadding(float maxShadowSize, float cornerRadius, 178 boolean addPaddingForCorners) { 179 if (addPaddingForCorners) { 180 return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius); 181 } else { 182 return maxShadowSize * SHADOW_MULTIPLIER; 183 } 184 } 185 186 static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius, 187 boolean addPaddingForCorners) { 188 if (addPaddingForCorners) { 189 return (float) (maxShadowSize + (1 - COS_45) * cornerRadius); 190 } else { 191 return maxShadowSize; 192 } 193 } 194 195 @Override 196 protected boolean onStateChange(int[] stateSet) { 197 final int newColor = mBackground.getColorForState(stateSet, mBackground.getDefaultColor()); 198 if (mPaint.getColor() == newColor) { 199 return false; 200 } 201 mPaint.setColor(newColor); 202 mDirty = true; 203 invalidateSelf(); 204 return true; 205 } 206 207 @Override 208 public boolean isStateful() { 209 return (mBackground != null && mBackground.isStateful()) || super.isStateful(); 210 } 211 212 @Override 213 public void setColorFilter(ColorFilter cf) { 214 mPaint.setColorFilter(cf); 215 } 216 217 @Override 218 public int getOpacity() { 219 return PixelFormat.TRANSLUCENT; 220 } 221 222 void setCornerRadius(float radius) { 223 if (radius < 0f) { 224 throw new IllegalArgumentException("Invalid radius " + radius + ". Must be >= 0"); 225 } 226 radius = (int) (radius + .5f); 227 if (mCornerRadius == radius) { 228 return; 229 } 230 mCornerRadius = radius; 231 mDirty = true; 232 invalidateSelf(); 233 } 234 235 @Override 236 public void draw(Canvas canvas) { 237 if (mDirty) { 238 buildComponents(getBounds()); 239 mDirty = false; 240 } 241 canvas.translate(0, mRawShadowSize / 2); 242 drawShadow(canvas); 243 canvas.translate(0, -mRawShadowSize / 2); 244 sRoundRectHelper.drawRoundRect(canvas, mCardBounds, mCornerRadius, mPaint); 245 } 246 247 private void drawShadow(Canvas canvas) { 248 final float edgeShadowTop = -mCornerRadius - mShadowSize; 249 final float inset = mCornerRadius + mInsetShadow + mRawShadowSize / 2; 250 final boolean drawHorizontalEdges = mCardBounds.width() - 2 * inset > 0; 251 final boolean drawVerticalEdges = mCardBounds.height() - 2 * inset > 0; 252 // LT 253 int saved = canvas.save(); 254 canvas.translate(mCardBounds.left + inset, mCardBounds.top + inset); 255 canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); 256 if (drawHorizontalEdges) { 257 canvas.drawRect(0, edgeShadowTop, 258 mCardBounds.width() - 2 * inset, -mCornerRadius, 259 mEdgeShadowPaint); 260 } 261 canvas.restoreToCount(saved); 262 // RB 263 saved = canvas.save(); 264 canvas.translate(mCardBounds.right - inset, mCardBounds.bottom - inset); 265 canvas.rotate(180f); 266 canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); 267 if (drawHorizontalEdges) { 268 canvas.drawRect(0, edgeShadowTop, 269 mCardBounds.width() - 2 * inset, -mCornerRadius + mShadowSize, 270 mEdgeShadowPaint); 271 } 272 canvas.restoreToCount(saved); 273 // LB 274 saved = canvas.save(); 275 canvas.translate(mCardBounds.left + inset, mCardBounds.bottom - inset); 276 canvas.rotate(270f); 277 canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); 278 if (drawVerticalEdges) { 279 canvas.drawRect(0, edgeShadowTop, 280 mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint); 281 } 282 canvas.restoreToCount(saved); 283 // RT 284 saved = canvas.save(); 285 canvas.translate(mCardBounds.right - inset, mCardBounds.top + inset); 286 canvas.rotate(90f); 287 canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); 288 if (drawVerticalEdges) { 289 canvas.drawRect(0, edgeShadowTop, 290 mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint); 291 } 292 canvas.restoreToCount(saved); 293 } 294 295 private void buildShadowCorners() { 296 RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius); 297 RectF outerBounds = new RectF(innerBounds); 298 outerBounds.inset(-mShadowSize, -mShadowSize); 299 300 if (mCornerShadowPath == null) { 301 mCornerShadowPath = new Path(); 302 } else { 303 mCornerShadowPath.reset(); 304 } 305 mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD); 306 mCornerShadowPath.moveTo(-mCornerRadius, 0); 307 mCornerShadowPath.rLineTo(-mShadowSize, 0); 308 // outer arc 309 mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false); 310 // inner arc 311 mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false); 312 mCornerShadowPath.close(); 313 float startRatio = mCornerRadius / (mCornerRadius + mShadowSize); 314 mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize, 315 new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, 316 new float[]{0f, startRatio, 1f}, 317 Shader.TileMode.CLAMP)); 318 319 // we offset the content shadowSize/2 pixels up to make it more realistic. 320 // this is why edge shadow shader has some extra space 321 // When drawing bottom edge shadow, we use that extra space. 322 mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0, 323 -mCornerRadius - mShadowSize, 324 new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, 325 new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP)); 326 mEdgeShadowPaint.setAntiAlias(false); 327 } 328 329 private void buildComponents(Rect bounds) { 330 // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift. 331 // We could have different top-bottom offsets to avoid extra gap above but in that case 332 // center aligning Views inside the CardView would be problematic. 333 final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER; 334 mCardBounds.set(bounds.left + mRawMaxShadowSize, bounds.top + verticalOffset, 335 bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset); 336 buildShadowCorners(); 337 } 338 339 float getCornerRadius() { 340 return mCornerRadius; 341 } 342 343 void getMaxShadowAndCornerPadding(Rect into) { 344 getPadding(into); 345 } 346 347 void setShadowSize(float size) { 348 setShadowSize(size, mRawMaxShadowSize); 349 } 350 351 void setMaxShadowSize(float size) { 352 setShadowSize(mRawShadowSize, size); 353 } 354 355 float getShadowSize() { 356 return mRawShadowSize; 357 } 358 359 float getMaxShadowSize() { 360 return mRawMaxShadowSize; 361 } 362 363 float getMinWidth() { 364 final float content = 2 365 * Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow + mRawMaxShadowSize / 2); 366 return content + (mRawMaxShadowSize + mInsetShadow) * 2; 367 } 368 369 float getMinHeight() { 370 final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow 371 + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2); 372 return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER + mInsetShadow) * 2; 373 } 374 375 void setColor(@Nullable ColorStateList color) { 376 setBackground(color); 377 invalidateSelf(); 378 } 379 380 ColorStateList getColor() { 381 return mBackground; 382 } 383 384 interface RoundRectHelper { 385 void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius, Paint paint); 386 } 387} 388