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.TypedArray;
21import android.graphics.Rect;
22import android.os.Build;
23import android.support.v7.cardview.R;
24import android.util.AttributeSet;
25import android.widget.FrameLayout;
26
27/**
28 * A FrameLayout with a rounded corner background and shadow.
29 * <p>
30 * CardView uses <code>elevation</code> property on L for shadows and falls back to a custom shadow
31 * implementation on older platforms.
32 * <p>
33 * Due to expensive nature of rounded corner clipping, on platforms before L, CardView does not
34 * clip its children that intersect with rounded corners. Instead, it adds padding to avoid such
35 * intersection (See {@link #setPreventCornerOverlap(boolean)} to change this behavior).
36 * <p>
37 * Before L, CardView adds padding to its content and draws shadows to that area. This padding
38 * amount is equal to <code>maxCardElevation + (1 - cos45) * cornerRadius</code> on the sides and
39 * <code>maxCardElevation * 1.5 + (1 - cos45) * cornerRadius</code> on top and bottom.
40 * <p>
41 * Since padding is used to offset content for shadows, you cannot set padding on CardView.
42 * Instead,
43 * you can use content padding attributes in XML or {@link #setContentPadding(int, int, int, int)}
44 * in code to set the padding between the edges of the Card and children of CardView.
45 * <p>
46 * Note that, if you specify exact dimensions for the CardView, because of the shadows, its content
47 * area will be different between platforms before L and after L. By using api version specific
48 * resource values, you can avoid these changes. Alternatively, If you want CardView to add inner
49 * padding on platforms L and after as well, you can set {@link #setUseCompatPadding(boolean)} to
50 * <code>true</code>.
51 * <p>
52 * To change CardView's elevation in a backward compatible way, use
53 * {@link #setCardElevation(float)}. CardView will use elevation API on L and before L, it will
54 * change the shadow size. To avoid moving the View while shadow size is changing, shadow size is
55 * clamped by {@link #getMaxCardElevation()}. If you want to change elevation dynamically, you
56 * should call {@link #setMaxCardElevation(float)} when CardView is initialized.
57 *
58 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor
59 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius
60 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation
61 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation
62 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardUseCompatPadding
63 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap
64 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPadding
65 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingLeft
66 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingTop
67 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingRight
68 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingBottom
69 */
70public class CardView extends FrameLayout implements CardViewDelegate {
71
72    private static final CardViewImpl IMPL;
73
74    static {
75        if (Build.VERSION.SDK_INT >= 21) {
76            IMPL = new CardViewApi21();
77        } else if (Build.VERSION.SDK_INT >= 17) {
78            IMPL = new CardViewJellybeanMr1();
79        } else {
80            IMPL = new CardViewEclairMr1();
81        }
82        IMPL.initStatic();
83    }
84
85    private boolean mCompatPadding;
86
87    private boolean mPreventCornerOverlap;
88
89    private final Rect mContentPadding = new Rect();
90
91    private final Rect mShadowBounds = new Rect();
92
93
94    public CardView(Context context) {
95        super(context);
96        initialize(context, null, 0);
97    }
98
99    public CardView(Context context, AttributeSet attrs) {
100        super(context, attrs);
101        initialize(context, attrs, 0);
102    }
103
104    public CardView(Context context, AttributeSet attrs, int defStyleAttr) {
105        super(context, attrs, defStyleAttr);
106        initialize(context, attrs, defStyleAttr);
107    }
108
109    @Override
110    public void setPadding(int left, int top, int right, int bottom) {
111        // NO OP
112    }
113
114    public void setPaddingRelative(int start, int top, int end, int bottom) {
115        // NO OP
116    }
117
118    /**
119     * Returns whether CardView will add inner padding on platforms L and after.
120     *
121     * @return True CardView adds inner padding on platforms L and after to have same dimensions
122     * with platforms before L.
123     */
124    @Override
125    public boolean getUseCompatPadding() {
126        return mCompatPadding;
127    }
128
129    /**
130     * CardView adds additional padding to draw shadows on platforms before L.
131     * <p>
132     * This may cause Cards to have different sizes between L and before L. If you need to align
133     * CardView with other Views, you may need api version specific dimension resources to account
134     * for the changes.
135     * As an alternative, you can set this flag to <code>true</code> and CardView will add the same
136     * padding values on platforms L and after.
137     * <p>
138     * Since setting this flag to true adds unnecessary gaps in the UI, default value is
139     * <code>false</code>.
140     *
141     * @param useCompatPadding True if CardView should add padding for the shadows on platforms L
142     *                         and above.
143     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardUseCompatPadding
144     */
145    public void setUseCompatPadding(boolean useCompatPadding) {
146        if (mCompatPadding == useCompatPadding) {
147            return;
148        }
149        mCompatPadding = useCompatPadding;
150        IMPL.onCompatPaddingChanged(this);
151    }
152
153    /**
154     * Sets the padding between the Card's edges and the children of CardView.
155     * <p>
156     * Depending on platform version or {@link #getUseCompatPadding()} settings, CardView may
157     * update these values before calling {@link android.view.View#setPadding(int, int, int, int)}.
158     *
159     * @param left   The left padding in pixels
160     * @param top    The top padding in pixels
161     * @param right  The right padding in pixels
162     * @param bottom The bottom padding in pixels
163     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPadding
164     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingLeft
165     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingTop
166     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingRight
167     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingBottom
168     */
169    public void setContentPadding(int left, int top, int right, int bottom) {
170        mContentPadding.set(left, top, right, bottom);
171        IMPL.updatePadding(this);
172    }
173
174    @Override
175    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
176        if (IMPL instanceof CardViewApi21 == false) {
177            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
178            switch (widthMode) {
179                case MeasureSpec.EXACTLY:
180                case MeasureSpec.AT_MOST:
181                    final int minWidth = (int) Math.ceil(IMPL.getMinWidth(this));
182                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minWidth,
183                            MeasureSpec.getSize(widthMeasureSpec)), widthMode);
184                    break;
185            }
186
187            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
188            switch (heightMode) {
189                case MeasureSpec.EXACTLY:
190                case MeasureSpec.AT_MOST:
191                    final int minHeight = (int) Math.ceil(IMPL.getMinHeight(this));
192                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight,
193                            MeasureSpec.getSize(heightMeasureSpec)), heightMode);
194                    break;
195            }
196            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
197        } else {
198            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
199        }
200    }
201
202    private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {
203        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
204                R.style.CardView_Light);
205        int backgroundColor = a.getColor(R.styleable.CardView_cardBackgroundColor, 0);
206        float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0);
207        float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0);
208        float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0);
209        mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false);
210        mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true);
211        int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0);
212        mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft,
213                defaultPadding);
214        mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop,
215                defaultPadding);
216        mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight,
217                defaultPadding);
218        mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom,
219                defaultPadding);
220        if (elevation > maxElevation) {
221            maxElevation = elevation;
222        }
223        a.recycle();
224        IMPL.initialize(this, context, backgroundColor, radius, elevation, maxElevation);
225    }
226
227    /**
228     * Updates the background color of the CardView
229     *
230     * @param color The new color to set for the card background
231     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor
232     */
233    public void setCardBackgroundColor(int color) {
234        IMPL.setBackgroundColor(this, color);
235    }
236
237    /**
238     * Returns the inner padding after the Card's left edge
239     *
240     * @return the inner padding after the Card's left edge
241     */
242    public int getContentPaddingLeft() {
243        return mContentPadding.left;
244    }
245
246    /**
247     * Returns the inner padding before the Card's right edge
248     *
249     * @return the inner padding before the Card's right edge
250     */
251    public int getContentPaddingRight() {
252        return mContentPadding.right;
253    }
254
255    /**
256     * Returns the inner padding after the Card's top edge
257     *
258     * @return the inner padding after the Card's top edge
259     */
260    public int getContentPaddingTop() {
261        return mContentPadding.top;
262    }
263
264    /**
265     * Returns the inner padding before the Card's bottom edge
266     *
267     * @return the inner padding before the Card's bottom edge
268     */
269    public int getContentPaddingBottom() {
270        return mContentPadding.bottom;
271    }
272
273    /**
274     * Updates the corner radius of the CardView.
275     *
276     * @param radius The radius in pixels of the corners of the rectangle shape
277     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius
278     * @see #setRadius(float)
279     */
280    public void setRadius(float radius) {
281        IMPL.setRadius(this, radius);
282    }
283
284    /**
285     * Returns the corner radius of the CardView.
286     *
287     * @return Corner radius of the CardView
288     * @see #getRadius()
289     */
290    public float getRadius() {
291        return IMPL.getRadius(this);
292    }
293
294    /**
295     * Internal method used by CardView implementations to update the padding.
296     *
297     * @hide
298     */
299    @Override
300    public void setShadowPadding(int left, int top, int right, int bottom) {
301        mShadowBounds.set(left, top, right, bottom);
302        super.setPadding(left + mContentPadding.left, top + mContentPadding.top,
303                right + mContentPadding.right, bottom + mContentPadding.bottom);
304    }
305
306    /**
307     * Updates the backward compatible elevation of the CardView.
308     *
309     * @param radius The backward compatible elevation in pixels.
310     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation
311     * @see #getCardElevation()
312     * @see #setMaxCardElevation(float)
313     */
314    public void setCardElevation(float radius) {
315        IMPL.setElevation(this, radius);
316    }
317
318    /**
319     * Returns the backward compatible elevation of the CardView.
320     *
321     * @return Elevation of the CardView
322     * @see #setCardElevation(float)
323     * @see #getMaxCardElevation()
324     */
325    public float getCardElevation() {
326        return IMPL.getElevation(this);
327    }
328
329    /**
330     * Updates the backward compatible elevation of the CardView.
331     * <p>
332     * Calling this method has no effect if device OS version is L or newer and
333     * {@link #getUseCompatPadding()} is <code>false</code>.
334     *
335     * @param radius The backward compatible elevation in pixels.
336     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation
337     * @see #setCardElevation(float)
338     * @see #getMaxCardElevation()
339     */
340    public void setMaxCardElevation(float radius) {
341        IMPL.setMaxElevation(this, radius);
342    }
343
344    /**
345     * Returns the backward compatible elevation of the CardView.
346     *
347     * @return Elevation of the CardView
348     * @see #setMaxCardElevation(float)
349     * @see #getCardElevation()
350     */
351    public float getMaxCardElevation() {
352        return IMPL.getMaxElevation(this);
353    }
354
355    /**
356     * Returns whether CardView should add extra padding to content to avoid overlaps with rounded
357     * corners on API versions 20 and below.
358     *
359     * @return True if CardView prevents overlaps with rounded corners on platforms before L.
360     *         Default value is <code>true</code>.
361     */
362    @Override
363    public boolean getPreventCornerOverlap() {
364        return mPreventCornerOverlap;
365    }
366
367    /**
368     * On API 20 and before, CardView does not clip the bounds of the Card for the rounded corners.
369     * Instead, it adds padding to content so that it won't overlap with the rounded corners.
370     * You can disable this behavior by setting this field to <code>false</code>.
371     * <p>
372     * Setting this value on API 21 and above does not have any effect unless you have enabled
373     * compatibility padding.
374     *
375     * @param preventCornerOverlap Whether CardView should add extra padding to content to avoid
376     *                             overlaps with the CardView corners.
377     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap
378     * @see #setUseCompatPadding(boolean)
379     */
380    public void setPreventCornerOverlap(boolean preventCornerOverlap) {
381        if (preventCornerOverlap == mPreventCornerOverlap) {
382            return;
383        }
384        mPreventCornerOverlap = preventCornerOverlap;
385        IMPL.onPreventCornerOverlapChanged(this);
386    }
387}
388