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