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.v7.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.TypedArray;
22import android.graphics.Color;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.os.Build;
26import android.support.annotation.ColorInt;
27import android.support.annotation.Nullable;
28import android.support.v7.cardview.R;
29import android.util.AttributeSet;
30import android.view.View;
31import 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 */
76public 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        }
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
109    public CardView(Context context) {
110        super(context);
111        initialize(context, null, 0);
112    }
113
114    public CardView(Context context, AttributeSet attrs) {
115        super(context, attrs);
116        initialize(context, attrs, 0);
117    }
118
119    public CardView(Context context, AttributeSet attrs, int defStyleAttr) {
120        super(context, attrs, defStyleAttr);
121        initialize(context, attrs, defStyleAttr);
122    }
123
124    @Override
125    public void setPadding(int left, int top, int right, int bottom) {
126        // NO OP
127    }
128
129    @Override
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     */
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     */
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     */
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
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
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
261    public void setMinimumWidth(int minWidth) {
262        mUserSetMinWidth = minWidth;
263        super.setMinimumWidth(minWidth);
264    }
265
266    @Override
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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