• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                 case MeasureSpec.UNSPECIFIED:
200                     // Do nothing
201                     break;
202             }
203 
204             final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
205             switch (heightMode) {
206                 case MeasureSpec.EXACTLY:
207                 case MeasureSpec.AT_MOST:
208                     final int minHeight = (int) Math.ceil(IMPL.getMinHeight(mCardViewDelegate));
209                     heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight,
210                             MeasureSpec.getSize(heightMeasureSpec)), heightMode);
211                     break;
212                 case MeasureSpec.UNSPECIFIED:
213                     // Do nothing
214                     break;
215             }
216             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
217         } else {
218             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
219         }
220     }
221 
initialize(Context context, AttributeSet attrs, int defStyleAttr)222     private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {
223         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
224                 R.style.CardView);
225         ColorStateList backgroundColor;
226         if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) {
227             backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor);
228         } else {
229             // There isn't one set, so we'll compute one based on the theme
230             final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR);
231             final int themeColorBackground = aa.getColor(0, 0);
232             aa.recycle();
233 
234             // If the theme colorBackground is light, use our own light color, otherwise dark
235             final float[] hsv = new float[3];
236             Color.colorToHSV(themeColorBackground, hsv);
237             backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f
238                     ? getResources().getColor(R.color.cardview_light_background)
239                     : getResources().getColor(R.color.cardview_dark_background));
240         }
241         float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0);
242         float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0);
243         float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0);
244         mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false);
245         mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true);
246         int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0);
247         mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft,
248                 defaultPadding);
249         mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop,
250                 defaultPadding);
251         mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight,
252                 defaultPadding);
253         mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom,
254                 defaultPadding);
255         if (elevation > maxElevation) {
256             maxElevation = elevation;
257         }
258         mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0);
259         mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0);
260         a.recycle();
261 
262         IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius,
263                 elevation, maxElevation);
264     }
265 
266     @Override
setMinimumWidth(int minWidth)267     public void setMinimumWidth(int minWidth) {
268         mUserSetMinWidth = minWidth;
269         super.setMinimumWidth(minWidth);
270     }
271 
272     @Override
setMinimumHeight(int minHeight)273     public void setMinimumHeight(int minHeight) {
274         mUserSetMinHeight = minHeight;
275         super.setMinimumHeight(minHeight);
276     }
277 
278     /**
279      * Updates the background color of the CardView
280      *
281      * @param color The new color to set for the card background
282      * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor
283      */
setCardBackgroundColor(@olorInt int color)284     public void setCardBackgroundColor(@ColorInt int color) {
285         IMPL.setBackgroundColor(mCardViewDelegate, ColorStateList.valueOf(color));
286     }
287 
288     /**
289      * Updates the background ColorStateList of the CardView
290      *
291      * @param color The new ColorStateList to set for the card background
292      * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor
293      */
setCardBackgroundColor(@ullable ColorStateList color)294     public void setCardBackgroundColor(@Nullable ColorStateList color) {
295         IMPL.setBackgroundColor(mCardViewDelegate, color);
296     }
297 
298     /**
299      * Returns the background color state list of the CardView.
300      *
301      * @return The background color state list of the CardView.
302      */
getCardBackgroundColor()303     public ColorStateList getCardBackgroundColor() {
304         return IMPL.getBackgroundColor(mCardViewDelegate);
305     }
306 
307     /**
308      * Returns the inner padding after the Card's left edge
309      *
310      * @return the inner padding after the Card's left edge
311      */
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      */
getContentPaddingRight()321     public int getContentPaddingRight() {
322         return mContentPadding.right;
323     }
324 
325     /**
326      * Returns the inner padding after the Card's top edge
327      *
328      * @return the inner padding after the Card's top edge
329      */
getContentPaddingTop()330     public int getContentPaddingTop() {
331         return mContentPadding.top;
332     }
333 
334     /**
335      * Returns the inner padding before the Card's bottom edge
336      *
337      * @return the inner padding before the Card's bottom edge
338      */
getContentPaddingBottom()339     public int getContentPaddingBottom() {
340         return mContentPadding.bottom;
341     }
342 
343     /**
344      * Updates the corner radius of the CardView.
345      *
346      * @param radius The radius in pixels of the corners of the rectangle shape
347      * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius
348      * @see #setRadius(float)
349      */
setRadius(float radius)350     public void setRadius(float radius) {
351         IMPL.setRadius(mCardViewDelegate, radius);
352     }
353 
354     /**
355      * Returns the corner radius of the CardView.
356      *
357      * @return Corner radius of the CardView
358      * @see #getRadius()
359      */
getRadius()360     public float getRadius() {
361         return IMPL.getRadius(mCardViewDelegate);
362     }
363 
364     /**
365      * Updates the backward compatible elevation of the CardView.
366      *
367      * @param elevation The backward compatible elevation in pixels.
368      * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation
369      * @see #getCardElevation()
370      * @see #setMaxCardElevation(float)
371      */
setCardElevation(float elevation)372     public void setCardElevation(float elevation) {
373         IMPL.setElevation(mCardViewDelegate, elevation);
374     }
375 
376     /**
377      * Returns the backward compatible elevation of the CardView.
378      *
379      * @return Elevation of the CardView
380      * @see #setCardElevation(float)
381      * @see #getMaxCardElevation()
382      */
getCardElevation()383     public float getCardElevation() {
384         return IMPL.getElevation(mCardViewDelegate);
385     }
386 
387     /**
388      * Updates the backward compatible maximum elevation of the CardView.
389      * <p>
390      * Calling this method has no effect if device OS version is Lollipop or newer and
391      * {@link #getUseCompatPadding()} is <code>false</code>.
392      *
393      * @param maxElevation The backward compatible maximum elevation in pixels.
394      * @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation
395      * @see #setCardElevation(float)
396      * @see #getMaxCardElevation()
397      */
setMaxCardElevation(float maxElevation)398     public void setMaxCardElevation(float maxElevation) {
399         IMPL.setMaxElevation(mCardViewDelegate, maxElevation);
400     }
401 
402     /**
403      * Returns the backward compatible maximum elevation of the CardView.
404      *
405      * @return Maximum elevation of the CardView
406      * @see #setMaxCardElevation(float)
407      * @see #getCardElevation()
408      */
getMaxCardElevation()409     public float getMaxCardElevation() {
410         return IMPL.getMaxElevation(mCardViewDelegate);
411     }
412 
413     /**
414      * Returns whether CardView should add extra padding to content to avoid overlaps with rounded
415      * corners on pre-Lollipop platforms.
416      *
417      * @return True if CardView prevents overlaps with rounded corners on platforms before Lollipop.
418      *         Default value is <code>true</code>.
419      */
getPreventCornerOverlap()420     public boolean getPreventCornerOverlap() {
421         return mPreventCornerOverlap;
422     }
423 
424     /**
425      * On pre-Lollipop platforms, CardView does not clip the bounds of the Card for the rounded
426      * corners. Instead, it adds padding to content so that it won't overlap with the rounded
427      * corners. You can disable this behavior by setting this field to <code>false</code>.
428      * <p>
429      * Setting this value on Lollipop and above does not have any effect unless you have enabled
430      * compatibility padding.
431      *
432      * @param preventCornerOverlap Whether CardView should add extra padding to content to avoid
433      *                             overlaps with the CardView corners.
434      * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap
435      * @see #setUseCompatPadding(boolean)
436      */
setPreventCornerOverlap(boolean preventCornerOverlap)437     public void setPreventCornerOverlap(boolean preventCornerOverlap) {
438         if (preventCornerOverlap != mPreventCornerOverlap) {
439             mPreventCornerOverlap = preventCornerOverlap;
440             IMPL.onPreventCornerOverlapChanged(mCardViewDelegate);
441         }
442     }
443 
444     private final CardViewDelegate mCardViewDelegate = new CardViewDelegate() {
445         private Drawable mCardBackground;
446 
447         @Override
448         public void setCardBackground(Drawable drawable) {
449             mCardBackground = drawable;
450             setBackgroundDrawable(drawable);
451         }
452 
453         @Override
454         public boolean getUseCompatPadding() {
455             return CardView.this.getUseCompatPadding();
456         }
457 
458         @Override
459         public boolean getPreventCornerOverlap() {
460             return CardView.this.getPreventCornerOverlap();
461         }
462 
463         @Override
464         public void setShadowPadding(int left, int top, int right, int bottom) {
465             mShadowBounds.set(left, top, right, bottom);
466             CardView.super.setPadding(left + mContentPadding.left, top + mContentPadding.top,
467                     right + mContentPadding.right, bottom + mContentPadding.bottom);
468         }
469 
470         @Override
471         public void setMinWidthHeightInternal(int width, int height) {
472             if (width > mUserSetMinWidth) {
473                 CardView.super.setMinimumWidth(width);
474             }
475             if (height > mUserSetMinHeight) {
476                 CardView.super.setMinimumHeight(height);
477             }
478         }
479 
480         @Override
481         public Drawable getCardBackground() {
482             return mCardBackground;
483         }
484 
485         @Override
486         public View getCardView() {
487             return CardView.this;
488         }
489     };
490 }
491