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