[go: nahoru, domu]

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 CardViewApi21();
84        } else if (Build.VERSION.SDK_INT >= 17) {
85            IMPL = new CardViewJellybeanMr1();
86        } else {
87            IMPL = new CardViewEclairMr1();
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    private int mUserSetMinWidth, mUserSetMinHeight;
104
105    private final Rect mContentPadding = new Rect();
106
107    private 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    public void setPaddingRelative(int start, int top, int end, int bottom) {
130        // NO OP
131    }
132
133    /**
134     * Returns whether CardView will add inner padding on platforms Lollipop and after.
135     *
136     * @return <code>true</code> if CardView adds inner padding on platforms Lollipop and after to
137     * have same dimensions with platforms before Lollipop.
138     */
139    public boolean getUseCompatPadding() {
140        return mCompatPadding;
141    }
142
143    /**
144     * CardView adds additional padding to draw shadows on platforms before Lollipop.
145     * <p>
146     * This may cause Cards to have different sizes between Lollipop and before Lollipop. If you
147     * need to align CardView with other Views, you may need api version specific dimension
148     * resources to account for the changes.
149     * As an alternative, you can set this flag to <code>true</code> and CardView will add the same
150     * padding values on platforms Lollipop and after.
151     * <p>
152     * Since setting this flag to true adds unnecessary gaps in the UI, default value is
153     * <code>false</code>.
154     *
155     * @param useCompatPadding <code>true></code> if CardView should add padding for the shadows on
156     *      platforms Lollipop and above.
157     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardUseCompatPadding
158     */
159    public void setUseCompatPadding(boolean useCompatPadding) {
160        if (mCompatPadding != useCompatPadding) {
161            mCompatPadding = useCompatPadding;
162            IMPL.onCompatPaddingChanged(mCardViewDelegate);
163        }
164    }
165
166    /**
167     * Sets the padding between the Card's edges and the children of CardView.
168     * <p>
169     * Depending on platform version or {@link #getUseCompatPadding()} settings, CardView may
170     * update these values before calling {@link android.view.View#setPadding(int, int, int, int)}.
171     *
172     * @param left   The left padding in pixels
173     * @param top    The top padding in pixels
174     * @param right  The right padding in pixels
175     * @param bottom The bottom padding in pixels
176     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPadding
177     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingLeft
178     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingTop
179     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingRight
180     * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingBottom
181     */
182    public void setContentPadding(int left, int top, int right, int bottom) {
183        mContentPadding.set(left, top, right, bottom);
184        IMPL.updatePadding(mCardViewDelegate);
185    }
186
187    @Override
188    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
189        if (!(IMPL instanceof CardViewApi21)) {
190            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
191            switch (widthMode) {
192                case MeasureSpec.EXACTLY:
193                case MeasureSpec.AT_MOST:
194                    final int minWidth = (int) Math.ceil(IMPL.getMinWidth(mCardViewDelegate));
195                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minWidth,
196                            MeasureSpec.getSize(widthMeasureSpec)), widthMode);
197                    break;
198            }
199
200            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
201            switch (heightMode) {
202                case MeasureSpec.EXACTLY:
203                case MeasureSpec.AT_MOST:
204                    final int minHeight = (int) Math.ceil(IMPL.getMinHeight(mCardViewDelegate));
205                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight,
206                            MeasureSpec.getSize(heightMeasureSpec)), heightMode);
207                    break;
208            }
209            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
210        } else {
211            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
212        }
213    }
214
215    private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {
216        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
217                R.style.CardView);
218        ColorStateList backgroundColor;
219        if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) {
220            backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor);
221        } else {
222            // There isn't one set, so we'll compute one based on the theme
223            final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR);
224            final int themeColorBackground = aa.getColor(0, 0);
225            aa.recycle();
226
227            // If the theme colorBackground is light, use our own light color, otherwise dark
228            final float[] hsv = new float[3];
229            Color.colorToHSV(themeColorBackground, hsv);
230            backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f
231                    ? getResources().getColor(R.color.cardview_light_background)
232                    : getResources().getColor(R.color.cardview_dark_background));
233        }
234        float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0);
235        float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0);
236        float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0);
237        mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false);
238        mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true);
239        int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0);
240        mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft,
241                defaultPadding);
242        mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop,
243                defaultPadding);
244        mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight,
245                defaultPadding);
246        mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom,
247                defaultPadding);
248        if (elevation > maxElevation) {
249            maxElevation = elevation;
250        }
251        mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0);
252        mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0);
253        a.recycle();
254
255        IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius,
256                elevation, maxElevation);
257    }
258
259    @Override
260    public void setMinimumWidth(int minWidth) {
261        mUserSetMinWidth = minWidth;
262        super.setMinimumWidth(minWidth);
263    }
264
265    @Override
266    public void setMinimumHeight(int minHeight) {
267        mUserSetMinHeight = minHeight;
268        super.setMinimumHeight(minHeight);
269    }
270
271    /**
272     * Updates the background color of the CardView
273     *
274     * @param color The new color to set for the card background
275     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor
276     */
277    public void setCardBackgroundColor(@ColorInt int color) {
278        IMPL.setBackgroundColor(mCardViewDelegate, ColorStateList.valueOf(color));
279    }
280
281    /**
282     * Updates the background ColorStateList of the CardView
283     *
284     * @param color The new ColorStateList to set for the card background
285     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor
286     */
287    public void setCardBackgroundColor(@Nullable ColorStateList color) {
288        IMPL.setBackgroundColor(mCardViewDelegate, color);
289    }
290
291    /**
292     * Returns the background color state list of the CardView.
293     *
294     * @return The background color state list of the CardView.
295     */
296    public ColorStateList getCardBackgroundColor() {
297        return IMPL.getBackgroundColor(mCardViewDelegate);
298    }
299
300    /**
301     * Returns the inner padding after the Card's left edge
302     *
303     * @return the inner padding after the Card's left edge
304     */
305    public int getContentPaddingLeft() {
306        return mContentPadding.left;
307    }
308
309    /**
310     * Returns the inner padding before the Card's right edge
311     *
312     * @return the inner padding before the Card's right edge
313     */
314    public int getContentPaddingRight() {
315        return mContentPadding.right;
316    }
317
318    /**
319     * Returns the inner padding after the Card's top edge
320     *
321     * @return the inner padding after the Card's top edge
322     */
323    public int getContentPaddingTop() {
324        return mContentPadding.top;
325    }
326
327    /**
328     * Returns the inner padding before the Card's bottom edge
329     *
330     * @return the inner padding before the Card's bottom edge
331     */
332    public int getContentPaddingBottom() {
333        return mContentPadding.bottom;
334    }
335
336    /**
337     * Updates the corner radius of the CardView.
338     *
339     * @param radius The radius in pixels of the corners of the rectangle shape
340     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius
341     * @see #setRadius(float)
342     */
343    public void setRadius(float radius) {
344        IMPL.setRadius(mCardViewDelegate, radius);
345    }
346
347    /**
348     * Returns the corner radius of the CardView.
349     *
350     * @return Corner radius of the CardView
351     * @see #getRadius()
352     */
353    public float getRadius() {
354        return IMPL.getRadius(mCardViewDelegate);
355    }
356
357    /**
358     * Updates the backward compatible elevation of the CardView.
359     *
360     * @param elevation The backward compatible elevation in pixels.
361     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation
362     * @see #getCardElevation()
363     * @see #setMaxCardElevation(float)
364     */
365    public void setCardElevation(float elevation) {
366        IMPL.setElevation(mCardViewDelegate, elevation);
367    }
368
369    /**
370     * Returns the backward compatible elevation of the CardView.
371     *
372     * @return Elevation of the CardView
373     * @see #setCardElevation(float)
374     * @see #getMaxCardElevation()
375     */
376    public float getCardElevation() {
377        return IMPL.getElevation(mCardViewDelegate);
378    }
379
380    /**
381     * Updates the backward compatible maximum elevation of the CardView.
382     * <p>
383     * Calling this method has no effect if device OS version is Lollipop or newer and
384     * {@link #getUseCompatPadding()} is <code>false</code>.
385     *
386     * @param maxElevation The backward compatible maximum elevation in pixels.
387     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation
388     * @see #setCardElevation(float)
389     * @see #getMaxCardElevation()
390     */
391    public void setMaxCardElevation(float maxElevation) {
392        IMPL.setMaxElevation(mCardViewDelegate, maxElevation);
393    }
394
395    /**
396     * Returns the backward compatible maximum elevation of the CardView.
397     *
398     * @return Maximum elevation of the CardView
399     * @see #setMaxCardElevation(float)
400     * @see #getCardElevation()
401     */
402    public float getMaxCardElevation() {
403        return IMPL.getMaxElevation(mCardViewDelegate);
404    }
405
406    /**
407     * Returns whether CardView should add extra padding to content to avoid overlaps with rounded
408     * corners on pre-Lollipop platforms.
409     *
410     * @return True if CardView prevents overlaps with rounded corners on platforms before Lollipop.
411     *         Default value is <code>true</code>.
412     */
413    public boolean getPreventCornerOverlap() {
414        return mPreventCornerOverlap;
415    }
416
417    /**
418     * On pre-Lollipop platforms, CardView does not clip the bounds of the Card for the rounded
419     * corners. Instead, it adds padding to content so that it won't overlap with the rounded
420     * corners. You can disable this behavior by setting this field to <code>false</code>.
421     * <p>
422     * Setting this value on Lollipop and above does not have any effect unless you have enabled
423     * compatibility padding.
424     *
425     * @param preventCornerOverlap Whether CardView should add extra padding to content to avoid
426     *                             overlaps with the CardView corners.
427     * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap
428     * @see #setUseCompatPadding(boolean)
429     */
430    public void setPreventCornerOverlap(boolean preventCornerOverlap) {
431        if (preventCornerOverlap != mPreventCornerOverlap) {
432            mPreventCornerOverlap = preventCornerOverlap;
433            IMPL.onPreventCornerOverlapChanged(mCardViewDelegate);
434        }
435    }
436
437    private final CardViewDelegate mCardViewDelegate = new CardViewDelegate() {
438        private Drawable mCardBackground;
439
440        @Override
441        public void setCardBackground(Drawable drawable) {
442            mCardBackground = drawable;
443            setBackgroundDrawable(drawable);
444        }
445
446        @Override
447        public boolean getUseCompatPadding() {
448            return CardView.this.getUseCompatPadding();
449        }
450
451        @Override
452        public boolean getPreventCornerOverlap() {
453            return CardView.this.getPreventCornerOverlap();
454        }
455
456        @Override
457        public void setShadowPadding(int left, int top, int right, int bottom) {
458            mShadowBounds.set(left, top, right, bottom);
459            CardView.super.setPadding(left + mContentPadding.left, top + mContentPadding.top,
460                    right + mContentPadding.right, bottom + mContentPadding.bottom);
461        }
462
463        @Override
464        public void setMinWidthHeightInternal(int width, int height) {
465            if (width > mUserSetMinWidth) {
466                CardView.super.setMinimumWidth(width);
467            }
468            if (height > mUserSetMinHeight) {
469                CardView.super.setMinimumHeight(height);
470            }
471        }
472
473        @Override
474        public Drawable getCardBackground() {
475            return mCardBackground;
476        }
477
478        @Override
479        public View getCardView() {
480            return CardView.this;
481        }
482    };
483}
484