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