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