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