/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.widget; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.ColorInt; import android.support.annotation.Nullable; import android.support.v7.cardview.R; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; /** * A FrameLayout with a rounded corner background and shadow. *
* CardView uses elevation
property on Lollipop for shadows and falls back to a
* custom emulated shadow implementation on older platforms.
*
* Due to expensive nature of rounded corner clipping, on platforms before Lollipop, CardView does * not clip its children that intersect with rounded corners. Instead, it adds padding to avoid such * intersection (See {@link #setPreventCornerOverlap(boolean)} to change this behavior). *
* Before Lollipop, CardView adds padding to its content and draws shadows to that area. This
* padding amount is equal to maxCardElevation + (1 - cos45) * cornerRadius
on the
* sides and maxCardElevation * 1.5 + (1 - cos45) * cornerRadius
on top and bottom.
*
* Since padding is used to offset content for shadows, you cannot set padding on CardView. * Instead, you can use content padding attributes in XML or * {@link #setContentPadding(int, int, int, int)} in code to set the padding between the edges of * the CardView and children of CardView. *
* Note that, if you specify exact dimensions for the CardView, because of the shadows, its content
* area will be different between platforms before Lollipop and after Lollipop. By using api version
* specific resource values, you can avoid these changes. Alternatively, If you want CardView to add
* inner padding on platforms Lollipop and after as well, you can call
* {@link #setUseCompatPadding(boolean)} and pass true
.
*
* To change CardView's elevation in a backward compatible way, use
* {@link #setCardElevation(float)}. CardView will use elevation API on Lollipop and before
* Lollipop, it will change the shadow size. To avoid moving the View while shadow size is changing,
* shadow size is clamped by {@link #getMaxCardElevation()}. If you want to change elevation
* dynamically, you should call {@link #setMaxCardElevation(float)} when CardView is initialized.
*
* @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor
* @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius
* @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation
* @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation
* @attr ref android.support.v7.cardview.R.styleable#CardView_cardUseCompatPadding
* @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap
* @attr ref android.support.v7.cardview.R.styleable#CardView_contentPadding
* @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingLeft
* @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingTop
* @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingRight
* @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingBottom
*/
public class CardView extends FrameLayout {
private static final int[] COLOR_BACKGROUND_ATTR = {android.R.attr.colorBackground};
private static final CardViewImpl IMPL;
static {
if (Build.VERSION.SDK_INT >= 21) {
IMPL = new CardViewApi21();
} else if (Build.VERSION.SDK_INT >= 17) {
IMPL = new CardViewJellybeanMr1();
} else {
IMPL = new CardViewGingerbread();
}
IMPL.initStatic();
}
private boolean mCompatPadding;
private boolean mPreventCornerOverlap;
/**
* CardView requires to have a particular minimum size to draw shadows before API 21. If
* developer also sets min width/height, they might be overridden.
*
* CardView works around this issue by recording user given parameters and using an internal
* method to set them.
*/
int mUserSetMinWidth, mUserSetMinHeight;
final Rect mContentPadding = new Rect();
final Rect mShadowBounds = new Rect();
public CardView(Context context) {
super(context);
initialize(context, null, 0);
}
public CardView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs, 0);
}
public CardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context, attrs, defStyleAttr);
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
// NO OP
}
public void setPaddingRelative(int start, int top, int end, int bottom) {
// NO OP
}
/**
* Returns whether CardView will add inner padding on platforms Lollipop and after.
*
* @return true
if CardView adds inner padding on platforms Lollipop and after to
* have same dimensions with platforms before Lollipop.
*/
public boolean getUseCompatPadding() {
return mCompatPadding;
}
/**
* CardView adds additional padding to draw shadows on platforms before Lollipop.
*
* This may cause Cards to have different sizes between Lollipop and before Lollipop. If you
* need to align CardView with other Views, you may need api version specific dimension
* resources to account for the changes.
* As an alternative, you can set this flag to true
and CardView will add the same
* padding values on platforms Lollipop and after.
*
* Since setting this flag to true adds unnecessary gaps in the UI, default value is
* false
.
*
* @param useCompatPadding true>
if CardView should add padding for the shadows on
* platforms Lollipop and above.
* @attr ref android.support.v7.cardview.R.styleable#CardView_cardUseCompatPadding
*/
public void setUseCompatPadding(boolean useCompatPadding) {
if (mCompatPadding != useCompatPadding) {
mCompatPadding = useCompatPadding;
IMPL.onCompatPaddingChanged(mCardViewDelegate);
}
}
/**
* Sets the padding between the Card's edges and the children of CardView.
*
* Depending on platform version or {@link #getUseCompatPadding()} settings, CardView may * update these values before calling {@link android.view.View#setPadding(int, int, int, int)}. * * @param left The left padding in pixels * @param top The top padding in pixels * @param right The right padding in pixels * @param bottom The bottom padding in pixels * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPadding * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingLeft * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingTop * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingRight * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingBottom */ public void setContentPadding(int left, int top, int right, int bottom) { mContentPadding.set(left, top, right, bottom); IMPL.updatePadding(mCardViewDelegate); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!(IMPL instanceof CardViewApi21)) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); switch (widthMode) { case MeasureSpec.EXACTLY: case MeasureSpec.AT_MOST: final int minWidth = (int) Math.ceil(IMPL.getMinWidth(mCardViewDelegate)); widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minWidth, MeasureSpec.getSize(widthMeasureSpec)), widthMode); break; } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); switch (heightMode) { case MeasureSpec.EXACTLY: case MeasureSpec.AT_MOST: final int minHeight = (int) Math.ceil(IMPL.getMinHeight(mCardViewDelegate)); heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight, MeasureSpec.getSize(heightMeasureSpec)), heightMode); break; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } private void initialize(Context context, AttributeSet attrs, int defStyleAttr) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr, R.style.CardView); ColorStateList backgroundColor; if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) { backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor); } else { // There isn't one set, so we'll compute one based on the theme final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR); final int themeColorBackground = aa.getColor(0, 0); aa.recycle(); // If the theme colorBackground is light, use our own light color, otherwise dark final float[] hsv = new float[3]; Color.colorToHSV(themeColorBackground, hsv); backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f ? getResources().getColor(R.color.cardview_light_background) : getResources().getColor(R.color.cardview_dark_background)); } float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0); float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0); float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0); mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false); mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true); int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0); mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft, defaultPadding); mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop, defaultPadding); mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight, defaultPadding); mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom, defaultPadding); if (elevation > maxElevation) { maxElevation = elevation; } mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0); mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0); a.recycle(); IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius, elevation, maxElevation); } @Override public void setMinimumWidth(int minWidth) { mUserSetMinWidth = minWidth; super.setMinimumWidth(minWidth); } @Override public void setMinimumHeight(int minHeight) { mUserSetMinHeight = minHeight; super.setMinimumHeight(minHeight); } /** * Updates the background color of the CardView * * @param color The new color to set for the card background * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor */ public void setCardBackgroundColor(@ColorInt int color) { IMPL.setBackgroundColor(mCardViewDelegate, ColorStateList.valueOf(color)); } /** * Updates the background ColorStateList of the CardView * * @param color The new ColorStateList to set for the card background * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor */ public void setCardBackgroundColor(@Nullable ColorStateList color) { IMPL.setBackgroundColor(mCardViewDelegate, color); } /** * Returns the background color state list of the CardView. * * @return The background color state list of the CardView. */ public ColorStateList getCardBackgroundColor() { return IMPL.getBackgroundColor(mCardViewDelegate); } /** * Returns the inner padding after the Card's left edge * * @return the inner padding after the Card's left edge */ public int getContentPaddingLeft() { return mContentPadding.left; } /** * Returns the inner padding before the Card's right edge * * @return the inner padding before the Card's right edge */ public int getContentPaddingRight() { return mContentPadding.right; } /** * Returns the inner padding after the Card's top edge * * @return the inner padding after the Card's top edge */ public int getContentPaddingTop() { return mContentPadding.top; } /** * Returns the inner padding before the Card's bottom edge * * @return the inner padding before the Card's bottom edge */ public int getContentPaddingBottom() { return mContentPadding.bottom; } /** * Updates the corner radius of the CardView. * * @param radius The radius in pixels of the corners of the rectangle shape * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius * @see #setRadius(float) */ public void setRadius(float radius) { IMPL.setRadius(mCardViewDelegate, radius); } /** * Returns the corner radius of the CardView. * * @return Corner radius of the CardView * @see #getRadius() */ public float getRadius() { return IMPL.getRadius(mCardViewDelegate); } /** * Updates the backward compatible elevation of the CardView. * * @param elevation The backward compatible elevation in pixels. * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation * @see #getCardElevation() * @see #setMaxCardElevation(float) */ public void setCardElevation(float elevation) { IMPL.setElevation(mCardViewDelegate, elevation); } /** * Returns the backward compatible elevation of the CardView. * * @return Elevation of the CardView * @see #setCardElevation(float) * @see #getMaxCardElevation() */ public float getCardElevation() { return IMPL.getElevation(mCardViewDelegate); } /** * Updates the backward compatible maximum elevation of the CardView. *
* Calling this method has no effect if device OS version is Lollipop or newer and
* {@link #getUseCompatPadding()} is false
.
*
* @param maxElevation The backward compatible maximum elevation in pixels.
* @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation
* @see #setCardElevation(float)
* @see #getMaxCardElevation()
*/
public void setMaxCardElevation(float maxElevation) {
IMPL.setMaxElevation(mCardViewDelegate, maxElevation);
}
/**
* Returns the backward compatible maximum elevation of the CardView.
*
* @return Maximum elevation of the CardView
* @see #setMaxCardElevation(float)
* @see #getCardElevation()
*/
public float getMaxCardElevation() {
return IMPL.getMaxElevation(mCardViewDelegate);
}
/**
* Returns whether CardView should add extra padding to content to avoid overlaps with rounded
* corners on pre-Lollipop platforms.
*
* @return True if CardView prevents overlaps with rounded corners on platforms before Lollipop.
* Default value is true
.
*/
public boolean getPreventCornerOverlap() {
return mPreventCornerOverlap;
}
/**
* On pre-Lollipop platforms, CardView does not clip the bounds of the Card for the rounded
* corners. Instead, it adds padding to content so that it won't overlap with the rounded
* corners. You can disable this behavior by setting this field to false
.
*
* Setting this value on Lollipop and above does not have any effect unless you have enabled * compatibility padding. * * @param preventCornerOverlap Whether CardView should add extra padding to content to avoid * overlaps with the CardView corners. * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap * @see #setUseCompatPadding(boolean) */ public void setPreventCornerOverlap(boolean preventCornerOverlap) { if (preventCornerOverlap != mPreventCornerOverlap) { mPreventCornerOverlap = preventCornerOverlap; IMPL.onPreventCornerOverlapChanged(mCardViewDelegate); } } private final CardViewDelegate mCardViewDelegate = new CardViewDelegate() { private Drawable mCardBackground; @Override public void setCardBackground(Drawable drawable) { mCardBackground = drawable; setBackgroundDrawable(drawable); } @Override public boolean getUseCompatPadding() { return CardView.this.getUseCompatPadding(); } @Override public boolean getPreventCornerOverlap() { return CardView.this.getPreventCornerOverlap(); } @Override public void setShadowPadding(int left, int top, int right, int bottom) { mShadowBounds.set(left, top, right, bottom); CardView.super.setPadding(left + mContentPadding.left, top + mContentPadding.top, right + mContentPadding.right, bottom + mContentPadding.bottom); } @Override public void setMinWidthHeightInternal(int width, int height) { if (width > mUserSetMinWidth) { CardView.super.setMinimumWidth(width); } if (height > mUserSetMinHeight) { CardView.super.setMinimumHeight(height); } } @Override public Drawable getCardBackground() { return mCardBackground; } @Override public View getCardView() { return CardView.this; } }; }