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