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 
17 package androidx.cardview.widget;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.TypedArray;
22 import android.graphics.Color;
23 import android.graphics.Rect;
24 import android.graphics.drawable.Drawable;
25 import android.os.Build;
26 import android.util.AttributeSet;
27 import android.view.View;
28 import android.widget.FrameLayout;
29 
30 import androidx.annotation.ColorInt;
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.Px;
34 import androidx.cardview.R;
35 
36 /**
37  * A FrameLayout with a rounded corner background and shadow.
38  * <p>
39  * CardView uses <code>elevation</code> property on Lollipop for shadows and falls back to a
40  * custom emulated shadow implementation on older platforms.
41  * <p>
42  * Due to expensive nature of rounded corner clipping, on platforms before Lollipop, CardView does
43  * not clip its children that intersect with rounded corners. Instead, it adds padding to avoid such
44  * intersection (See {@link #setPreventCornerOverlap(boolean)} to change this behavior).
45  * <p>
46  * Before Lollipop, CardView adds padding to its content and draws shadows to that area. This
47  * padding amount is equal to <code>maxCardElevation + (1 - cos45) * cornerRadius</code> on the
48  * sides and <code>maxCardElevation * 1.5 + (1 - cos45) * cornerRadius</code> on top and bottom.
49  * <p>
50  * Since padding is used to offset content for shadows, you cannot set padding on CardView.
51  * Instead, you can use content padding attributes in XML or
52  * {@link #setContentPadding(int, int, int, int)} in code to set the padding between the edges of
53  * the CardView and children of CardView.
54  * <p>
55  * Note that, if you specify exact dimensions for the CardView, because of the shadows, its content
56  * area will be different between platforms before Lollipop and after Lollipop. By using api version
57  * specific resource values, you can avoid these changes. Alternatively, If you want CardView to add
58  * inner padding on platforms Lollipop and after as well, you can call
59  * {@link #setUseCompatPadding(boolean)} and pass <code>true</code>.
60  * <p>
61  * To change CardView's elevation in a backward compatible way, use
62  * {@link #setCardElevation(float)}. CardView will use elevation API on Lollipop and before
63  * Lollipop, it will change the shadow size. To avoid moving the View while shadow size is changing,
64  * shadow size is clamped by {@link #getMaxCardElevation()}. If you want to change elevation
65  * dynamically, you should call {@link #setMaxCardElevation(float)} when CardView is initialized.
66  *
67  * @attr ref androidx.cardview.R.styleable#CardView_cardBackgroundColor
68  * @attr ref androidx.cardview.R.styleable#CardView_cardCornerRadius
69  * @attr ref androidx.cardview.R.styleable#CardView_cardElevation
70  * @attr ref androidx.cardview.R.styleable#CardView_cardMaxElevation
71  * @attr ref androidx.cardview.R.styleable#CardView_cardUseCompatPadding
72  * @attr ref androidx.cardview.R.styleable#CardView_cardPreventCornerOverlap
73  * @attr ref androidx.cardview.R.styleable#CardView_contentPadding
74  * @attr ref androidx.cardview.R.styleable#CardView_contentPaddingLeft
75  * @attr ref androidx.cardview.R.styleable#CardView_contentPaddingTop
76  * @attr ref androidx.cardview.R.styleable#CardView_contentPaddingRight
77  * @attr ref androidx.cardview.R.styleable#CardView_contentPaddingBottom
78  */
79 public class CardView extends FrameLayout {
80 
81     private static final int[] COLOR_BACKGROUND_ATTR = {android.R.attr.colorBackground};
82     private static final CardViewImpl IMPL;
83 
84     static {
85         if (Build.VERSION.SDK_INT >= 21) {
86             IMPL = new CardViewApi21Impl();
87         } else if (Build.VERSION.SDK_INT >= 17) {
88             IMPL = new CardViewApi17Impl();
89         } else {
90             IMPL = new CardViewBaseImpl();
91         }
IMPL.initStatic()92         IMPL.initStatic();
93     }
94 
95     private boolean mCompatPadding;
96 
97     private boolean mPreventCornerOverlap;
98 
99     /**
100      * CardView requires to have a particular minimum size to draw shadows before API 21. If
101      * developer also sets min width/height, they might be overridden.
102      *
103      * CardView works around this issue by recording user given parameters and using an internal
104      * method to set them.
105      */
106     int mUserSetMinWidth, mUserSetMinHeight;
107 
108     final Rect mContentPadding = new Rect();
109 
110     final Rect mShadowBounds = new Rect();
111 
CardView(@onNull Context context)112     public CardView(@NonNull Context context) {
113         this(context, null);
114     }
115 
CardView(@onNull Context context, @Nullable AttributeSet attrs)116     public CardView(@NonNull Context context, @Nullable AttributeSet attrs) {
117         this(context, attrs, R.attr.cardViewStyle);
118     }
119 
CardView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)120     public CardView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
121         super(context, attrs, defStyleAttr);
122 
123         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
124                 R.style.CardView);
125         ColorStateList backgroundColor;
126         if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) {
127             backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor);
128         } else {
129             // There isn't one set, so we'll compute one based on the theme
130             final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR);
131             final int themeColorBackground = aa.getColor(0, 0);
132             aa.recycle();
133 
134             // If the theme colorBackground is light, use our own light color, otherwise dark
135             final float[] hsv = new float[3];
136             Color.colorToHSV(themeColorBackground, hsv);
137             backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f
138                     ? getResources().getColor(R.color.cardview_light_background)
139                     : getResources().getColor(R.color.cardview_dark_background));
140         }
141         float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0);
142         float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0);
143         float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0);
144         mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false);
145         mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true);
146         int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0);
147         mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft,
148                 defaultPadding);
149         mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop,
150                 defaultPadding);
151         mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight,
152                 defaultPadding);
153         mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom,
154                 defaultPadding);
155         if (elevation > maxElevation) {
156             maxElevation = elevation;
157         }
158         mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0);
159         mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0);
160         a.recycle();
161 
162         IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius,
163                 elevation, maxElevation);
164     }
165 
166     @Override
setPadding(int left, int top, int right, int bottom)167     public void setPadding(int left, int top, int right, int bottom) {
168         // NO OP
169     }
170 
171     @Override
setPaddingRelative(int start, int top, int end, int bottom)172     public void setPaddingRelative(int start, int top, int end, int bottom) {
173         // NO OP
174     }
175 
176     /**
177      * Returns whether CardView will add inner padding on platforms Lollipop and after.
178      *
179      * @return <code>true</code> if CardView adds inner padding on platforms Lollipop and after to
180      * have same dimensions with platforms before Lollipop.
181      */
getUseCompatPadding()182     public boolean getUseCompatPadding() {
183         return mCompatPadding;
184     }
185 
186     /**
187      * CardView adds additional padding to draw shadows on platforms before Lollipop.
188      * <p>
189      * This may cause Cards to have different sizes between Lollipop and before Lollipop. If you
190      * need to align CardView with other Views, you may need api version specific dimension
191      * resources to account for the changes.
192      * As an alternative, you can set this flag to <code>true</code> and CardView will add the same
193      * padding values on platforms Lollipop and after.
194      * <p>
195      * Since setting this flag to true adds unnecessary gaps in the UI, default value is
196      * <code>false</code>.
197      *
198      * @param useCompatPadding <code>true></code> if CardView should add padding for the shadows on
199      *      platforms Lollipop and above.
200      * @attr ref androidx.cardview.R.styleable#CardView_cardUseCompatPadding
201      */
setUseCompatPadding(boolean useCompatPadding)202     public void setUseCompatPadding(boolean useCompatPadding) {
203         if (mCompatPadding != useCompatPadding) {
204             mCompatPadding = useCompatPadding;
205             IMPL.onCompatPaddingChanged(mCardViewDelegate);
206         }
207     }
208 
209     /**
210      * Sets the padding between the Card's edges and the children of CardView.
211      * <p>
212      * Depending on platform version or {@link #getUseCompatPadding()} settings, CardView may
213      * update these values before calling {@link android.view.View#setPadding(int, int, int, int)}.
214      *
215      * @param left   The left padding in pixels
216      * @param top    The top padding in pixels
217      * @param right  The right padding in pixels
218      * @param bottom The bottom padding in pixels
219      * @attr ref androidx.cardview.R.styleable#CardView_contentPadding
220      * @attr ref androidx.cardview.R.styleable#CardView_contentPaddingLeft
221      * @attr ref androidx.cardview.R.styleable#CardView_contentPaddingTop
222      * @attr ref androidx.cardview.R.styleable#CardView_contentPaddingRight
223      * @attr ref androidx.cardview.R.styleable#CardView_contentPaddingBottom
224      */
setContentPadding(@x int left, @Px int top, @Px int right, @Px int bottom)225     public void setContentPadding(@Px int left, @Px int top, @Px int right, @Px int bottom) {
226         mContentPadding.set(left, top, right, bottom);
227         IMPL.updatePadding(mCardViewDelegate);
228     }
229 
230     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)231     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
232         if (!(IMPL instanceof CardViewApi21Impl)) {
233             final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
234             switch (widthMode) {
235                 case MeasureSpec.EXACTLY:
236                 case MeasureSpec.AT_MOST:
237                     final int minWidth = (int) Math.ceil(IMPL.getMinWidth(mCardViewDelegate));
238                     widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minWidth,
239                             MeasureSpec.getSize(widthMeasureSpec)), widthMode);
240                     break;
241                 case MeasureSpec.UNSPECIFIED:
242                     // Do nothing
243                     break;
244             }
245 
246             final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
247             switch (heightMode) {
248                 case MeasureSpec.EXACTLY:
249                 case MeasureSpec.AT_MOST:
250                     final int minHeight = (int) Math.ceil(IMPL.getMinHeight(mCardViewDelegate));
251                     heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight,
252                             MeasureSpec.getSize(heightMeasureSpec)), heightMode);
253                     break;
254                 case MeasureSpec.UNSPECIFIED:
255                     // Do nothing
256                     break;
257             }
258             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
259         } else {
260             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
261         }
262     }
263 
264     @Override
setMinimumWidth(int minWidth)265     public void setMinimumWidth(int minWidth) {
266         mUserSetMinWidth = minWidth;
267         super.setMinimumWidth(minWidth);
268     }
269 
270     @Override
setMinimumHeight(int minHeight)271     public void setMinimumHeight(int minHeight) {
272         mUserSetMinHeight = minHeight;
273         super.setMinimumHeight(minHeight);
274     }
275 
276     /**
277      * Updates the background color of the CardView
278      *
279      * @param color The new color to set for the card background
280      * @attr ref androidx.cardview.R.styleable#CardView_cardBackgroundColor
281      */
setCardBackgroundColor(@olorInt int color)282     public void setCardBackgroundColor(@ColorInt int color) {
283         IMPL.setBackgroundColor(mCardViewDelegate, ColorStateList.valueOf(color));
284     }
285 
286     /**
287      * Updates the background ColorStateList of the CardView
288      *
289      * @param color The new ColorStateList to set for the card background
290      * @attr ref androidx.cardview.R.styleable#CardView_cardBackgroundColor
291      */
setCardBackgroundColor(@ullable ColorStateList color)292     public void setCardBackgroundColor(@Nullable ColorStateList color) {
293         IMPL.setBackgroundColor(mCardViewDelegate, color);
294     }
295 
296     /**
297      * Returns the background color state list of the CardView.
298      *
299      * @return The background color state list of the CardView.
300      */
301     @NonNull
getCardBackgroundColor()302     public ColorStateList getCardBackgroundColor() {
303         return IMPL.getBackgroundColor(mCardViewDelegate);
304     }
305 
306     /**
307      * Returns the inner padding after the Card's left edge
308      *
309      * @return the inner padding after the Card's left edge
310      */
311     @Px
getContentPaddingLeft()312     public int getContentPaddingLeft() {
313         return mContentPadding.left;
314     }
315 
316     /**
317      * Returns the inner padding before the Card's right edge
318      *
319      * @return the inner padding before the Card's right edge
320      */
321     @Px
getContentPaddingRight()322     public int getContentPaddingRight() {
323         return mContentPadding.right;
324     }
325 
326     /**
327      * Returns the inner padding after the Card's top edge
328      *
329      * @return the inner padding after the Card's top edge
330      */
331     @Px
getContentPaddingTop()332     public int getContentPaddingTop() {
333         return mContentPadding.top;
334     }
335 
336     /**
337      * Returns the inner padding before the Card's bottom edge
338      *
339      * @return the inner padding before the Card's bottom edge
340      */
341     @Px
getContentPaddingBottom()342     public int getContentPaddingBottom() {
343         return mContentPadding.bottom;
344     }
345 
346     /**
347      * Updates the corner radius of the CardView.
348      *
349      * @param radius The radius in pixels of the corners of the rectangle shape
350      * @attr ref androidx.cardview.R.styleable#CardView_cardCornerRadius
351      * @see #setRadius(float)
352      */
setRadius(float radius)353     public void setRadius(float radius) {
354         IMPL.setRadius(mCardViewDelegate, radius);
355     }
356 
357     /**
358      * Returns the corner radius of the CardView.
359      *
360      * @return Corner radius of the CardView
361      * @see #getRadius()
362      */
getRadius()363     public float getRadius() {
364         return IMPL.getRadius(mCardViewDelegate);
365     }
366 
367     /**
368      * Updates the backward compatible elevation of the CardView.
369      *
370      * @param elevation The backward compatible elevation in pixels.
371      * @attr ref androidx.cardview.R.styleable#CardView_cardElevation
372      * @see #getCardElevation()
373      * @see #setMaxCardElevation(float)
374      */
setCardElevation(float elevation)375     public void setCardElevation(float elevation) {
376         IMPL.setElevation(mCardViewDelegate, elevation);
377     }
378 
379     /**
380      * Returns the backward compatible elevation of the CardView.
381      *
382      * @return Elevation of the CardView
383      * @see #setCardElevation(float)
384      * @see #getMaxCardElevation()
385      */
getCardElevation()386     public float getCardElevation() {
387         return IMPL.getElevation(mCardViewDelegate);
388     }
389 
390     /**
391      * Updates the backward compatible maximum elevation of the CardView.
392      * <p>
393      * Calling this method has no effect if device OS version is Lollipop or newer and
394      * {@link #getUseCompatPadding()} is <code>false</code>.
395      *
396      * @param maxElevation The backward compatible maximum elevation in pixels.
397      * @attr ref androidx.cardview.R.styleable#CardView_cardMaxElevation
398      * @see #setCardElevation(float)
399      * @see #getMaxCardElevation()
400      */
setMaxCardElevation(float maxElevation)401     public void setMaxCardElevation(float maxElevation) {
402         IMPL.setMaxElevation(mCardViewDelegate, maxElevation);
403     }
404 
405     /**
406      * Returns the backward compatible maximum elevation of the CardView.
407      *
408      * @return Maximum elevation of the CardView
409      * @see #setMaxCardElevation(float)
410      * @see #getCardElevation()
411      */
getMaxCardElevation()412     public float getMaxCardElevation() {
413         return IMPL.getMaxElevation(mCardViewDelegate);
414     }
415 
416     /**
417      * Returns whether CardView should add extra padding to content to avoid overlaps with rounded
418      * corners on pre-Lollipop platforms.
419      *
420      * @return True if CardView prevents overlaps with rounded corners on platforms before Lollipop.
421      *         Default value is <code>true</code>.
422      */
getPreventCornerOverlap()423     public boolean getPreventCornerOverlap() {
424         return mPreventCornerOverlap;
425     }
426 
427     /**
428      * On pre-Lollipop platforms, CardView does not clip the bounds of the Card for the rounded
429      * corners. Instead, it adds padding to content so that it won't overlap with the rounded
430      * corners. You can disable this behavior by setting this field to <code>false</code>.
431      * <p>
432      * Setting this value on Lollipop and above does not have any effect unless you have enabled
433      * compatibility padding.
434      *
435      * @param preventCornerOverlap Whether CardView should add extra padding to content to avoid
436      *                             overlaps with the CardView corners.
437      * @attr ref androidx.cardview.R.styleable#CardView_cardPreventCornerOverlap
438      * @see #setUseCompatPadding(boolean)
439      */
setPreventCornerOverlap(boolean preventCornerOverlap)440     public void setPreventCornerOverlap(boolean preventCornerOverlap) {
441         if (preventCornerOverlap != mPreventCornerOverlap) {
442             mPreventCornerOverlap = preventCornerOverlap;
443             IMPL.onPreventCornerOverlapChanged(mCardViewDelegate);
444         }
445     }
446 
447     private final CardViewDelegate mCardViewDelegate = new CardViewDelegate() {
448         private Drawable mCardBackground;
449 
450         @Override
451         public void setCardBackground(Drawable drawable) {
452             mCardBackground = drawable;
453             setBackgroundDrawable(drawable);
454         }
455 
456         @Override
457         public boolean getUseCompatPadding() {
458             return CardView.this.getUseCompatPadding();
459         }
460 
461         @Override
462         public boolean getPreventCornerOverlap() {
463             return CardView.this.getPreventCornerOverlap();
464         }
465 
466         @Override
467         public void setShadowPadding(int left, int top, int right, int bottom) {
468             mShadowBounds.set(left, top, right, bottom);
469             CardView.super.setPadding(left + mContentPadding.left, top + mContentPadding.top,
470                     right + mContentPadding.right, bottom + mContentPadding.bottom);
471         }
472 
473         @Override
474         public void setMinWidthHeightInternal(int width, int height) {
475             if (width > mUserSetMinWidth) {
476                 CardView.super.setMinimumWidth(width);
477             }
478             if (height > mUserSetMinHeight) {
479                 CardView.super.setMinimumHeight(height);
480             }
481         }
482 
483         @Override
484         public Drawable getCardBackground() {
485             return mCardBackground;
486         }
487 
488         @Override
489         public View getCardView() {
490             return CardView.this;
491         }
492     };
493 }
494