CardView 擴(kuò)展 FrameLayout 類并讓您能夠顯示卡片內(nèi)的信息蹭沛,這些信息在整個(gè)平臺(tái)中擁有一致的呈現(xiàn)方式纳胧。CardView 小部件可擁有陰影和圓角汗销。
如果要使用陰影創(chuàng)建卡片卵慰,請(qǐng)使用 card_view:cardElevation 屬性缩歪。CardView 在 Android 5.0(API 級(jí)別 21)及更高版本中使用真實(shí)高度與動(dòng)態(tài)陰影灵莲,而在早期的 Android 版本中則返回編程陰影實(shí)現(xiàn)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="160dp"
android:scaleType="centerCrop"
android:src="@drawable/balon" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/card_title"
android:textColor="#000"
android:textSize="18sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_content"
android:textColor="#555" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_share"
android:theme="@style/PrimaryFlatButton" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_explore"
android:theme="@style/PrimaryFlatButton" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</RelativeLayout>
CardView的常用屬性:
屬性 | 作用 |
---|---|
card_view:cardElevation | 陰影的大小 |
card_view:cardMaxElevation | 陰影最大高度 |
card_view:cardBackgroundColor | 卡片的背景色 |
card_view:cardCornerRadius | 卡片的圓角大小 |
card_view:contentPadding | 卡片內(nèi)容于邊距的間隔 |
card_view:contentPaddingBottom | 卡片內(nèi)容與底部的邊距 |
card_view:contentPaddingTop | 卡片內(nèi)容與頂部的邊距 |
card_view:contentPaddingLeft | 卡片內(nèi)容與左邊的邊距 |
card_view:contentPaddingRight | 卡片內(nèi)容與右邊的邊距 |
card_view:contentPaddingStart | 卡片內(nèi)容于邊距的間隔起始 |
card_view:contentPaddingEnd | 卡片內(nèi)容于邊距的間隔終止 |
card_view:cardUseCompatPadding | 設(shè)置內(nèi)邊距咖刃,android5.0(代號(hào):Lollipop,API level 21)及以上的版本和之前的版本仍舊具有一樣的計(jì)算方式 |
card_view:cardPreventConrerOverlap | 在android5.0之前的版本中添加內(nèi)邊距苟翻,這個(gè)屬性為了防止內(nèi)容和邊角的重疊 |
CardView的使用比較簡單的止,網(wǎng)上也有相當(dāng)多的文章可以參考檩坚,本文在此不做過多的闡述。
CardView的兼容性考慮诅福,在不同版本的系統(tǒng)上實(shí)現(xiàn)有差異匾委,大家在使用時(shí)要考慮到這一點(diǎn),我們先看幾個(gè)小Demo氓润。
cardPreventConrerOverlap屬性:
Lollipop以下版本赂乐,cardPreventConrerOverlap = false,不設(shè)置contentPadding,如圖,內(nèi)容和圓角重疊咖气。
Lollipop以下版本挨措,cardPreventConrerOverlap = true,不設(shè)置contentPadding,如圖,添加了額外padding防止內(nèi)容和圓角為重疊崩溪。
cardUseCompatPadding屬性:
為了展示出效果浅役,我將elevation設(shè)置的比較大,測試設(shè)備nexus4 768x1280伶唯。
下圖左側(cè)為Lollipop以下版本觉既,右側(cè)為Lollipop版本,cardUseCompatPadding = false。
下圖左側(cè)為Lollipop以下版本瞪讼,右側(cè)為Lollipop版本钧椰,cardUseCompatPadding = true。
可見符欠,如果想讓Lollipop版本及以上的內(nèi)邊距和Lollipop版本以下相同嫡霞,就需要把該屬性設(shè)置為true.。
但該屬性的影響沒你想的那么簡單背亥。
我們?cè)偻季种刑砑右粋€(gè)控件秒际,cardUseCompatPadding= false悬赏。
這差距夠明顯吧狡汉!那我們?cè)鯓颖WC各個(gè)版本的顯示效果相同呢?設(shè)置cardUseCompatPadding=true闽颇。
這下就ok了盾戴,其實(shí)導(dǎo)致這種問題出現(xiàn)的根本原因是view的陰影效果在不同版本實(shí)現(xiàn)方式存在差異,下文分析源碼時(shí)會(huì)講到兵多。
這個(gè)問題還有一種解決方式尖啡。不設(shè)置cardUseCompatPadding屬性為true,在Lollipop版本以下對(duì)應(yīng)的dimens.xml中填寫cardview的margin = 0dp,在Lollipop版本下(即values-21文件夾)的dimens.xml設(shè)置需要的值剩膘。
如果你想給CardView指定明確的寬高呢衅斩?
什么內(nèi)容區(qū)域大小居然不一樣!這種問題要被測試美眉發(fā)現(xiàn)豈不是太沒面子怠褐,怎么解決呢畏梆?設(shè)置cardUseCompatPadding屬性為true。
同理這這個(gè)問題也可以通過不同系統(tǒng)版本dimens.xml來適配奈懒。
總結(jié)一下
CardView通過elevation屬性來設(shè)置view的陰影奠涌,但Lollipop之前的版本是模擬實(shí)現(xiàn),即實(shí)現(xiàn)方式不同磷杏。
因?yàn)椴眉舯容^耗費(fèi)性能溜畅,所以Lollipop之前的版本不對(duì)內(nèi)部View進(jìn)行裁剪,通過添加padding的方式避免內(nèi)部View和圓角重疊极祸。使用setPreventCornerOverlap方法或?qū)?yīng)xml card_view:cardPreventConrerOverlap屬性可更改這一行為慈格,該屬性默認(rèn)為true。
Lollipop之前的版本遥金,CardView和內(nèi)容之間添加邊距浴捆,并在該區(qū)域繪制陰影,兩邊的間距為maxCardElevation + (1 - cos45) * cornerRadius汰规,上下的間距為maxCardElevation * 1.5 + (1 - cos45) * cornerRadius汤功。
因?yàn)閜adding屬性被用來做偏移繪制陰影,所以不能使用CardView的padding屬性溜哮,如果想設(shè)置CardView和其子View之間的邊距滔金,可使用setContentPadding(int, int, int, int)方法或?qū)?yīng)的xml屬性色解。
如果對(duì)CardView設(shè)置了明確的尺寸,因?yàn)殛幱暗木壒什鸵穑鋬?nèi)容區(qū)域在Lollipop版本和之前的版本上顯示不同科阎,你可以通過不同系統(tǒng)版本使用不同資源值或設(shè)置useCompatPadding屬性為true的方式來避免此問題。
通過setCardElevation(float)以兼容的方式設(shè)置CardView的elevation,CardView會(huì)使用Lollipop下或之前版本下的elevation API忿族,進(jìn)而改變陰影的尺寸锣笨。為防止改變陰影尺寸時(shí),view發(fā)生移動(dòng)道批,陰影大小不會(huì)超過MaxCardElevation错英,如果想在CardView初始化后動(dòng)態(tài)改變陰影大小,應(yīng)使用setMaxCardElevation(float)方法隆豹。
解剖源碼椭岩。
結(jié)構(gòu)圖如下:
CardView內(nèi)部根據(jù)不同版本系統(tǒng)實(shí)例化對(duì)應(yīng)的CardViewImpl對(duì)象,CardViewImpl對(duì)象通過CardViewDelegate對(duì)象與CardView交互璃赡。
CardView的靜態(tài)代碼塊中根據(jù)系統(tǒng)版本實(shí)例化對(duì)應(yīng)的實(shí)現(xiàn)判哥。
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();
}
這里調(diào)用了initStatic(),API21中即CardViewApi21類中是空實(shí)現(xiàn)碉考,API17中實(shí)現(xiàn)如下(CardViewJellybeanMr1):
public void initStatic() {
RoundRectDrawableWithShadow.sRoundRectHelper
= new RoundRectDrawableWithShadow.RoundRectHelper() {
@Override
public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
Paint paint) {
canvas.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
}
};
}
API17之前的版本實(shí)現(xiàn)如下(CardViewGingerbread):
public void initStatic() {
//使用7步繪制操作來繪制出圓角矩形塌计,在API17之前的版本此種方式要比canvas.drawRoundRect快,
//因?yàn)锳PI 11-16使用了alpha蒙版紋理去繪制
RoundRectDrawableWithShadow.sRoundRectHelper =
new RoundRectDrawableWithShadow.RoundRectHelper() {
@Override
public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
Paint paint) {
final float twoRadius = cornerRadius * 2;
final float innerWidth = bounds.width() - twoRadius - 1;
final float innerHeight = bounds.height() - twoRadius - 1;
if (cornerRadius >= 1f) {
// increment corner radius to account for half pixels.
float roundedCornerRadius = cornerRadius + .5f;
sCornerRect.set(-roundedCornerRadius, -roundedCornerRadius, roundedCornerRadius,
roundedCornerRadius);
int saved = canvas.save();
canvas.translate(bounds.left + roundedCornerRadius,
bounds.top + roundedCornerRadius);
canvas.drawArc(sCornerRect, 180, 90, true, paint);
canvas.translate(innerWidth, 0);
canvas.rotate(90);
canvas.drawArc(sCornerRect, 180, 90, true, paint);
canvas.translate(innerHeight, 0);
canvas.rotate(90);
canvas.drawArc(sCornerRect, 180, 90, true, paint);
canvas.translate(innerWidth, 0);
canvas.rotate(90);
canvas.drawArc(sCornerRect, 180, 90, true, paint);
canvas.restoreToCount(saved);
//繪制上下兩部分
canvas.drawRect(bounds.left + roundedCornerRadius - 1f, bounds.top,
bounds.right - roundedCornerRadius + 1f,
bounds.top + roundedCornerRadius, paint);
canvas.drawRect(bounds.left + roundedCornerRadius - 1f,
bounds.bottom - roundedCornerRadius,
bounds.right - roundedCornerRadius + 1f, bounds.bottom, paint);
}
// 繪制中間部分
canvas.drawRect(bounds.left, bounds.top + cornerRadius,
bounds.right, bounds.bottom - cornerRadius , paint);
}
};
}
API17和之前版本的陰影實(shí)現(xiàn)差異主要在這里,因?yàn)樾蕟栴}侯谁,API17之前的版本使用分步繪制測方式繪制圓角矩形锌仅。
CardView的構(gòu)造器中會(huì)調(diào)用initialize(...),該方法中主要拿到各屬性,然后調(diào)用具體實(shí)現(xiàn)的初始化方法良蒸。
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 {
// 沒有設(shè)置背景則從當(dāng)前主題中提取
final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR);
final int themeColorBackground = aa.getColor(0, 0);
aa.recycle();
//若主題中的colorBackground是淺色,使用cardview_light_background,否則使用cardview_dark_background
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);
...
mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0);
a.recycle();
IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius,
elevation, maxElevation);
}
接下來我們帶著問題來看源碼技扼。
- API19之前版本如何實(shí)現(xiàn)陰影?
- API19及以上版本如何實(shí)現(xiàn)View裁切嫩痰?
- API19及之后版本如何實(shí)現(xiàn)陰影剿吻?
- API19之前版本cardPreventConrerOverlap屬性的影響?
- API19及以上版本受cardUseCompatPadding屬性的影響串纺?
- 為什么陰影在在x軸方向和y軸方向發(fā)生了位移丽旅,而不是均勻分布在view四周?
問題1:API19之前版本如何實(shí)現(xiàn)陰影纺棺?
接下來我們先看API19之前是怎么實(shí)現(xiàn)陰影的(CardViewGingerbread類)榄笙。
@Override
public void initialize(CardViewDelegate cardView, Context context,
ColorStateList backgroundColor, float radius, float elevation, float maxElevation) {
RoundRectDrawableWithShadow background = createBackground(context, backgroundColor, radius,
elevation, maxElevation);
background.setAddPaddingForCorners(cardView.getPreventCornerOverlap());
cardView.setCardBackground(background);
updatePadding(cardView);
}
看來它的陰影是由RoundRectDrawableWithShadow類來實(shí)現(xiàn)的。
我們來看看來它的陰影是由RoundRectDrawableWithShadow類來實(shí)現(xiàn)的onDraw,第一次調(diào)用要走buildComponents方法祷蝌。
public void draw(Canvas canvas) {
if (mDirty) {
buildComponents(getBounds());
mDirty = false;
}
canvas.translate(0, mRawShadowSize / 2);
drawShadow(canvas);
canvas.translate(0, -mRawShadowSize / 2);
sRoundRectHelper.drawRoundRect(canvas, mCardBounds, mCornerRadius, mPaint);
}
buildComponents方法中茅撞,mRawMaxShadowSize其實(shí)就是maxElevation,此處確定了cardview的邊界,上下左右都進(jìn)行了偏移米丘,空出來的區(qū)域是為了繪制陰影剑令。
private void buildComponents(Rect bounds) {
// Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
// We could have different top-bottom offsets to avoid extra gap above but in that case
// center aligning Views inside the CardView would be problematic.
final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER;
mCardBounds.set(bounds.left + mRawMaxShadowSize, bounds.top+verticalOffset,
bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset);
buildShadowCorners();
}
buildShadowCorners方法中初始化了繪制邊陰影和角陰影的path。
private void buildShadowCorners() {
RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
RectF outerBounds = new RectF(innerBounds);
outerBounds.inset(-mShadowSize, -mShadowSize);
if (mCornerShadowPath == null) {
mCornerShadowPath = new Path();
} else {
mCornerShadowPath.reset();
}
mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
mCornerShadowPath.moveTo(-mCornerRadius, 0);
mCornerShadowPath.rLineTo(-mShadowSize, 0);
// outer arc
mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
// inner arc
mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
mCornerShadowPath.close();
float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, startRatio, 1f}
, Shader.TileMode.CLAMP));
// we offset the content shadowSize/2 pixels up to make it more realistic.
// this is why edge shadow shader has some extra space
// When drawing bottom edge shadow, we use that extra space.
mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0,
-mCornerRadius - mShadowSize,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
mEdgeShadowPaint.setAntiAlias(false);
}
接下來看drawShadow方法拄查,這里基本的canvas操作吁津。
private void drawShadow(Canvas canvas) {
final float edgeShadowTop = -mCornerRadius - mShadowSize;
final float inset = mCornerRadius + mInsetShadow + mRawShadowSize / 2;
final boolean drawHorizontalEdges = mCardBounds.width() - 2 * inset > 0;
final boolean drawVerticalEdges = mCardBounds.height() - 2 * inset > 0;
// LT
int saved = canvas.save();
canvas.translate(mCardBounds.left + inset, mCardBounds.top + inset);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
if (drawHorizontalEdges) {
canvas.drawRect(0, edgeShadowTop,
mCardBounds.width() - 2 * inset, -mCornerRadius,
mEdgeShadowPaint);
}
canvas.restoreToCount(saved);
// RB
saved = canvas.save();
canvas.translate(mCardBounds.right - inset, mCardBounds.bottom - inset);
canvas.rotate(180f);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
if (drawHorizontalEdges) {
canvas.drawRect(0, edgeShadowTop,
mCardBounds.width() - 2 * inset, -mCornerRadius + mShadowSize,
mEdgeShadowPaint);
}
canvas.restoreToCount(saved);
// LB
saved = canvas.save();
canvas.translate(mCardBounds.left + inset, mCardBounds.bottom - inset);
canvas.rotate(270f);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
if (drawVerticalEdges) {
canvas.drawRect(0, edgeShadowTop,
mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint);
}
canvas.restoreToCount(saved);
// RT
saved = canvas.save();
canvas.translate(mCardBounds.right - inset, mCardBounds.top + inset);
canvas.rotate(90f);
canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
if (drawVerticalEdges) {
canvas.drawRect(0, edgeShadowTop,
mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint);
}
canvas.restoreToCount(saved);
}
可見API19之前陰影的實(shí)現(xiàn)是由canvas+path+RadialGradient繪制角陰影,canvas+path+LinearGradient繪制邊陰影。
問題2:API19及以上版本如何實(shí)現(xiàn)View裁切堕扶?
進(jìn)入CardViewApi21類碍脏,
@Override
public void initialize(CardViewDelegate cardView, Context context,
ColorStateList backgroundColor, float radius, float elevation, float maxElevation) {
final RoundRectDrawable background = new RoundRectDrawable(backgroundColor, radius);
cardView.setCardBackground(background);
View view = cardView.getCardView();
view.setClipToOutline(true);
view.setElevation(elevation);
setMaxElevation(cardView, maxElevation);
}
這里實(shí)例化了RoundRectDrawable作為cardview的背景。RoundRectDrawable是用來繪制背景圓角矩形稍算,
cardView.getCardView()拿到了CardView對(duì)象典尾,前面我們說了CardView是繼承自FrameLayout的,所以CardView即是ViewGroup也是View,view.setClipToOutline(true)是什么意思呢邪蛔?
android5.0之后允許自定義視圖陰影與輪廓
視圖的背景可繪制對(duì)象的邊界將決定其陰影的默認(rèn)形狀急黎。輪廓代表圖形對(duì)象的外形并定義觸摸反饋的波紋區(qū)域。
下面舉一個(gè)以背景可繪制對(duì)象定義的視圖示例:
<TextView
android:id="@+id/myview"
...
android:elevation="2dp"
android:background="@drawable/myrect" />
背景可繪制對(duì)象被定義為一個(gè)擁有圓角的矩形:
<!-- res/drawable/myrect.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#42000000" />
<corners android:radius="5dp" />
</shape>
視圖將投射一個(gè)帶有圓角的陰影侧到,因?yàn)楸尘翱衫L制對(duì)象將定義視圖的輪廓。 如果提供一個(gè)自定義輪廓淤击,則此輪廓將替換視圖陰影的默認(rèn)形狀匠抗。
如果要為代碼中的視圖定義自定義輪廓:
擴(kuò)展 ViewOutlineProvider 類別。
替代 getOutline() 方法污抬。
利用 View.setOutlineProvider() 方法向您的視圖指定新的輪廓提供程序汞贸。
您可使用 Outline 類別中的方法創(chuàng)建帶有圓角的橢圓形和矩形輪廓。視圖的默認(rèn)輪廓提供程序?qū)囊晥D背景取得輪廓印机。 如果要防止視圖投射陰影矢腻,請(qǐng)將其輪廓提供程序設(shè)置為 null。
裁剪視圖
裁剪視圖讓您能夠輕松改變視圖形狀射赛。您可以裁剪視圖多柑,以便與其他設(shè)計(jì)元素保持一致,也可以根據(jù)用戶輸入改變視圖形狀楣责。您可使用 View.setClipToOutline() 方法或 android:clipToOutline 屬性將視圖裁剪至其輪廓區(qū)域竣灌。 由 Outline.canClip() 方法所決定,僅有矩形秆麸、圓形和圓角矩形輪廓支持裁剪初嘹。
如果要將視圖裁剪至可繪制對(duì)象的形狀,請(qǐng)將可繪制對(duì)象設(shè)置為視圖背景(如上所示)并調(diào)用 View.setClipToOutline() 方法沮趣。
問題3: API19及之后版本如何實(shí)現(xiàn)陰影屯烦?
CardViewApi21的initialize方法中調(diào)用了view.setElevation(elevation),
public void setElevation(float elevation) {
if (elevation != getElevation()) {
invalidateViewProperty(true, false);
mRenderNode.setElevation(elevation);
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
}
}
再看mRenderNode.setElevation(elevation);
public boolean setElevation(float lift) {
return nSetElevation(mNativeRenderNode, lift);
}
nSetElevation(mNativeRenderNode, lift)是個(gè)native方法,由此可見android5.0開始所有的view都可以顯示陰影,而且是根據(jù)elevation屬性直接有native方法來實(shí)現(xiàn)驻龟。
android5.0開始因?yàn)榧尤肓薓aterial Design,Material Design 為 UI 元素引入高度甸箱,為View加上了Z屬性。
由 Z 屬性所表示的視圖高度將決定其陰影的視覺外觀:擁有較高 Z 值的視圖將投射更大且更柔和的陰影迅脐。 擁有較高 Z 值的視圖將擋住擁有較低 Z 值的視圖芍殖;不過視圖的 Z 值并不影響視圖的大小。
視圖的 Z 值包含兩個(gè)組件:
高度:靜態(tài)組件谴蔑。
轉(zhuǎn)換:用于動(dòng)畫的動(dòng)態(tài)組件豌骏。
Z = elevation + translationZ
所以影響View陰影的因素有兩個(gè)elevation和translationZ.
在 Material Design Guidelines 中有建議卡片、按鈕這類元素觸摸時(shí)應(yīng)當(dāng)有一個(gè)浮起的效果隐锭,也就是增大 Z 軸位移,我們?cè)趺磳?shí)現(xiàn)這個(gè)效果呢窃躲?
只需要借助 Lollipop 的一個(gè)新屬性 android:stateListAnimator,創(chuàng)建一個(gè) TranslationZ 的變換動(dòng)畫放在 /res/anim,自己取一個(gè)名(如 touch_raise.xml)钦睡,加入以下內(nèi)容:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:state_pressed="true">
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="@dimen/touch_raise"
android:valueType="floatType" />
</item>
<item>
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="0dp"
android:valueType="floatType" />
</item>
</selector>
然后為你需要添加效果的 CardView(其他 View 同理)所在的 Layout XML 復(fù)制多一份到 /res/layout-v21蒂窒,然后在新的那份 XML 的 CardView 中加入屬性 android:stateListAnimator="@anim/touch_raise"。這樣荞怒,你的卡片按住時(shí)就會(huì)有浮起(陰影加深)的效果了洒琢。
至于波紋效果只需要給CardView加上android:foreground="?attr/selectableItemBackground" 屬性即可。
問題4:API19之前版本cardPreventConrerOverlap屬性的影響褐桌?
CardView的setPreventCornerOverlap方法衰抑。
public void setPreventCornerOverlap(boolean preventCornerOverlap) {
if (preventCornerOverlap != mPreventCornerOverlap) {
mPreventCornerOverlap = preventCornerOverlap;
IMPL.onPreventCornerOverlapChanged(mCardViewDelegate);
}
}
然后看CardViewGingerbread的onPreventCornerOverlapChanged方法。
@Override
public void onPreventCornerOverlapChanged(CardViewDelegate cardView) {
getShadowBackground(cardView).setAddPaddingForCorners(cardView.getPreventCornerOverlap());
updatePadding(cardView);
}
一路跟蹤下來荧嵌,發(fā)現(xiàn)關(guān)鍵點(diǎn)在RoundRectDrawableWithShadow中呛踊,addPaddingForCorners即為傳過來的preventCornerOverlap,當(dāng)preventCornerOverlap為true時(shí)啦撮,內(nèi)邊距增加了(1 - COS_45) * cornerRadius)谭网,這樣CardView的子View就不會(huì)和圓角重疊了。
static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
boolean addPaddingForCorners) {
if (addPaddingForCorners) {
return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
} else {
return maxShadowSize * SHADOW_MULTIPLIER;
}
}
問題5 API19及以上版本受cardUseCompatPadding屬性的影響赃春?
CardView的setUseCompatPadding方法愉择。
public void setUseCompatPadding(boolean useCompatPadding) {
if (mCompatPadding != useCompatPadding) {
mCompatPadding = useCompatPadding;
IMPL.onCompatPaddingChanged(mCardViewDelegate);
}
}
進(jìn)入CardViewApi21。
@Override
public void onCompatPaddingChanged(CardViewDelegate cardView) {
setMaxElevation(cardView, getMaxElevation(cardView));
}
最后跟蹤到RoundRectDrawable,這里的mInsetForPadding就是cardUseCompatPadding屬性的值聘鳞,當(dāng)cardUseCompatPadding屬性為true時(shí)薄辅,會(huì)設(shè)置內(nèi)邊距,calculateVerticalPadding和calculateHorizontalPadding方法是RoundRectDrawableWithShadow的靜態(tài)方法抠璃,如此5.0和之前版本就具有相同的內(nèi)邊距計(jì)算方式了站楚。
private void updateBounds(Rect bounds) {
if (bounds == null) {
bounds = getBounds();
}
mBoundsF.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
mBoundsI.set(bounds);
if (mInsetForPadding) {
float vInset = calculateVerticalPadding(mPadding, mRadius, mInsetForRadius);
float hInset = calculateHorizontalPadding(mPadding, mRadius, mInsetForRadius);
mBoundsI.inset((int) Math.ceil(hInset), (int) Math.ceil(vInset));
// to make sure they have same bounds.
mBoundsF.set(mBoundsI);
}
}
在CardViewApi21的updatePadding方法也可以看到,如果不設(shè)置cardUseCompatPadding,其陰影內(nèi)邊距為0搏嗡,這也就解釋了前文中的現(xiàn)象窿春。
@Override
public void updatePadding(CardViewDelegate cardView) {
if (!cardView.getUseCompatPadding()) {
cardView.setShadowPadding(0, 0, 0, 0);
return;
}
float elevation = getMaxElevation(cardView);
final float radius = getRadius(cardView);
int hPadding = (int) Math.ceil(RoundRectDrawableWithShadow
.calculateHorizontalPadding(elevation, radius, cardView.getPreventCornerOverlap()));
int vPadding = (int) Math.ceil(RoundRectDrawableWithShadow
.calculateVerticalPadding(elevation, radius, cardView.getPreventCornerOverlap()));
cardView.setShadowPadding(hPadding, vPadding, hPadding, vPadding);
}
問題6 為什么陰影在在x軸方向和y軸方向發(fā)生了位移拉一,而不是均勻分布在view四周?
在RoundRectDrawableWithShadow的draw方法中旧乞,我們看到蔚润,在繪制陰影前,畫布向y軸正方向進(jìn)行了位移尺栖,這就使得陰影的方向發(fā)生了變化嫡纠。
@Override
public void draw(Canvas canvas) {
if (mDirty) {
buildComponents(getBounds());
mDirty = false;
}
canvas.translate(0, mRawShadowSize / 2);
drawShadow(canvas);
canvas.translate(0, -mRawShadowSize / 2);
sRoundRectHelper.drawRoundRect(canvas, mCardBounds, mCornerRadius, mPaint);
}
如果項(xiàng)目的設(shè)計(jì)符合Material Design,那最好延赌,如果設(shè)計(jì)有一天讓你實(shí)現(xiàn)四周帶相同尺寸陰影的效果呢除盏?我們也知道怎么做了吧!
這里我把實(shí)現(xiàn)方式放到Github上了挫以,有需要?dú)g迎關(guān)注者蠕。
參考資料:
https://developer.android.com/training/material/lists-cards.html#Dependencies
http://www.reibang.com/p/33b1d21d6ba6
https://developer.android.com/training/material/shadows-clipping.html#Shadows
https://android.jlelse.eu/android-card-view-edb905e67cd6
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/1025/3621.html