可設定陰影顏色的 CardView
image
需求由來
最近專案中來了新的需求,一大堆卡片式佈局,還有不同的陰影顏色,甚至不同的狀態下顏色還不一樣,UI 給的切圖各種錯位,而 Google 的CardView 是無法設定陰影顏色的,我能怎麼辦,我也很絕望啊(:з」∠)
沒辦法,百度了一堆都沒有找到解決方案,最後借鑑了各位大神的思路,才有了這篇文章。
實現思路
怎麼實現?當然是參照 Google 的CardView 啦,畢竟是親生的,各種實現優化都是非常棒的,然後就是在CardView 的基礎上,給他新增上設定陰影顏色的功能,很簡單的是吧。
開始幹活啦
-
那麼先新建一個專案,然後把CardView 目錄下的所有類檔案、資原始檔 都複製到新建的專案中。
螢幕快照 2018-10-10 10.19.58.png
-
這樣,這個CardView 就已經可以使用了,和原生的一模一樣,同樣不能設定陰影顏色,這當然不是我們需要的。
-
我們的目標是讓CardView 能夠支援設定陰影顏色,那麼首先我們就來探查一下CardView 的顏色資原始檔。
螢幕快照 2018-10-10 10.23.16.png
<color name="cardview_shadow_end_color">#03000000</color> <color name="cardview_shadow_start_color">#37000000</color>
-
這是什麼?陰影的開始顏色和結束顏色?這麼簡單就解決了嗎?我們可以嘗試修改這兩個顏色值,然而,並沒有效果(O_O)? 這是為什麼?
-
我們來檢視這兩個資源在哪些地方被使用。
螢幕快照 2018-10-10 10.30.23.png
-
這個RoundRectDrawableWithShadow 是一個整合Drawable 類,我們在檢視一下這個類在哪裡被使用。
螢幕快照 2018-10-10 10.35.53.png
-
他在CardViewBaseImpl 中被建立,和這個類似的還有CardViewApi17Impl 和CardViewApi21Impl ,其中CardViewApi17Impl 是繼承CardViewBaseImpl 的,我們接下來再看看這兩個類是在哪裡使用的。
螢幕快照 2018-10-10 10.39.21.png
-
我們在CardView 中發現了他,嗯、、好像是 API 小於21才會使用,我們用一個API 19的模擬器試試。
螢幕快照 2018-10-10 10.45.11.png
-
我們成功了,陰影的顏色被改變了,看樣子之前之所以沒有效果是因為模擬器的API大於等於21了,那麼在高版本上我們改如何實現這種效果呢?
-
首先,我們來看一下,使用程式碼設定CardView 高度時進行的操作。
public void setCardElevation(float elevation) { IMPL.setElevation(mCardViewDelegate, elevation); }
- 從上面的步驟我們可以知道,當 API 低於21時,CardView 使用CardViewBaseImpl 和CardViewApi17Impl 來處理的,高於或等於21時使用CardViewApi21Impl 來處理。這裡面有什麼區別呢?
// CardViewBaseImpl @Override public void setElevation(CardViewDelegate cardView, float elevation) { getShadowBackground(cardView).setShadowSize(elevation); } // CardViewApi21Impl @Override public void setElevation(CardViewDelegate cardView, float elevation) { cardView.getCardView().setElevation(elevation); }
- 我們可以看到,當 API 高於或等於21時,使用的是從API21開始才有的Elevation屬性設定陰影效果的,而低於21時是通過Drawable來繪製陰影效果。弄清楚了這些,我們就可以開始給CardView 的陰影新增顏色啦。
實現可設定陰影顏色的 CardView
- 首先,我們需要給CardView 新增兩條屬性,用來設定陰影的開始顏色和結束顏色。
<attr name="cardShadowColorStart" format="color" /> <attr name="cardShadowColorEnd" format="color" />
- 然後,在CardView 的構造方法裡面獲取屬性。
ColorStateList shadowColorStart = a.getColorStateList(R.styleable.CardView_cardShadowColorStart); ColorStateList shaodwColorEnd = a.getColorStateList(R.styleable.CardView_cardShadowColorEnd);
- 這裡為了支援狀態選擇器,使用ColorStateList
- CardView 的構造方法最後會呼叫CardViewImpl 的initialize() 方法進行初始化,因此在initialize() 方法中新增 兩個引數。
void initialize(CardViewDelegate cardView, Context context, ColorStateList backgroundColor, float radius, float elevation, float maxElevation, ColorStateList shadowColorStart, ColorStateList shadowColorEnd);
- 修改RoundRectDrawableWithShadow 構造方法,新增陰影顏色引數,並進行處理。
// 陰影顏色預設是 int 型別的顏色值,要修改成 ColorStateList private final ColorStateList mShadowStartColor; private final ColorStateList mShadowEndColor; RoundRectDrawableWithShadow(Resources resources, ColorStateList backgroundColor, float radius, float shadowSize, float maxShadowSize, ColorStateList shadowColorStart, ColorStateList shadowColorEnd) { // 如果沒有設定陰影顏色,使用預設顏色 if (shadowColorStart == null) { mShadowStartColor = ColorStateList.valueOf(resources.getColor(R.color.cardview_shadow_start_color)); } else { mShadowStartColor = shadowColorStart; } if (shadowColorEnd == null) { mShadowEndColor = ColorStateList.valueOf(resources.getColor(R.color.cardview_shadow_end_color)); } else { mShadowEndColor = shadowColorEnd; } mInsetShadow = resources.getDimensionPixelSize(R.dimen.cardview_compat_inset_shadow); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); setBackground(backgroundColor); mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); mCornerShadowPaint.setStyle(Paint.Style.FILL); mCornerRadius = (int) (radius + .5f); mCardBounds = new RectF(); mEdgeShadowPaint = new Paint(mCornerShadowPaint); mEdgeShadowPaint.setAntiAlias(false); setShadowSize(shadowSize, maxShadowSize); } 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); // 獲取當前狀態下的陰影顏色 int starColor = mShadowStartColor.getColorForState(getState(), mShadowStartColor.getDefaultColor()); int endColor = mShadowEndColor.getColorForState(getState(), mShadowEndColor.getDefaultColor()); // 設定陰影顏色 mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize, new int[]{starColor, starColor, endColor}, 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[]{starColor, starColor, endColor}, new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP)); mEdgeShadowPaint.setAntiAlias(false); }
- 好了,這樣就可以直接使用屬性配置陰影顏色了,當然,還是隻能在低版本使用。
<cn.wj.android.colorcardview.CardView android:layout_width="200dp" android:layout_height="50dp" app:cardPreventCornerOverlap="true" app:cardBackgroundColor="#069ff1" app:cardShadowColorStart="#2dfd0000" app:cardShadowColorEnd="#03fd0000" app:cardUseCompatPadding="true" app:cardElevation="8dp" />
適配高版本
- 經過上面的步驟之後,我們的CardView 已經可以通過屬性設定陰影顏色了,同時支援狀態選擇器,但是,這些效果僅僅在API低於21時有效。那麼接下來我們來處理高版本的陰影顏色。
- 我們知道,高版本中,CardView 的陰影效果是通過CardViewApi21Impl 處理的,其內部是通過 API21 才加入的 Elevation 屬性設定的,而 Elevation 這個屬性,Google 並沒有提供任何介面來對其陰影顏色進行設定。
-
我的解決思路是,既然高版本的 Elevation 沒有提供方法,那麼我們就使用低版本的方案處理。
螢幕快照 2018-10-10 11.37.54.png
- 我們可以看到,CardViewApi21Impl 是直接實現了CardViewImpl 介面的,而低版本的實現是在CardViewBaseImpl 中的,CardViewApi17Impl 繼承CardViewBaseImpl ,那麼我們可以讓CardViewApi21Impl 直接繼承CardViewApi17Impl 。
- 同時進行判斷,如果自定義了陰影顏色,那麼就是用父類的實現,即低版本的實現,否則,依舊使用高版本的實現。
@RequiresApi(21) class CardViewApi21Impl extends CardViewApi17Impl { // 標記 - 是否使用低版本實現 private boolean useLower = false; @Override public void initialize(CardViewDelegate cardView, Context context, ColorStateList backgroundColor, float radius, float elevation, float maxElevation, ColorStateList shadowColorStart, ColorStateList shadowColorEnd) { // 沒有自定義陰影顏色,不使用低版本實現 if (shadowColorStart == null && shadowColorEnd == null) { useLower = false; final RoundRectDrawable background = new RoundRectDrawable(backgroundColor, radius); cardView.setCardBackground(background); View view = cardView.getCardView(); view.setClipToOutline(true); view.setElevation(elevation); setMaxElevation(cardView, maxElevation); } else { // 配置了自定義顏色,使用低版本實現 useLower = true; super.initialize(cardView, context, backgroundColor, radius, elevation, maxElevation, shadowColorStart, shadowColorEnd); } } @Override public void setRadius(CardViewDelegate cardView, float radius) { if (useLower) { super.setRadius(cardView, radius); } else { getCardBackground(cardView).setRadius(radius); } } @Override public void setMaxElevation(CardViewDelegate cardView, float maxElevation) { if (useLower) { super.setMaxElevation(cardView, maxElevation); } else { getCardBackground(cardView).setPadding(maxElevation, cardView.getUseCompatPadding(), cardView.getPreventCornerOverlap()); updatePadding(cardView); } } @Override public float getMaxElevation(CardViewDelegate cardView) { if (useLower) { return super.getMaxElevation(cardView); } else { return getCardBackground(cardView).getPadding(); } } @Override public float getMinWidth(CardViewDelegate cardView) { if (useLower) { return super.getMinWidth(cardView); } else { return getRadius(cardView) * 2; } } @Override public float getMinHeight(CardViewDelegate cardView) { if (useLower) { return super.getMinHeight(cardView); } else { return getRadius(cardView) * 2; } } @Override public float getRadius(CardViewDelegate cardView) { if (useLower) { return super.getRadius(cardView); } else { return getCardBackground(cardView).getRadius(); } } @Override public void setElevation(CardViewDelegate cardView, float elevation) { if (useLower) { super.setElevation(cardView, elevation); } else { cardView.getCardView().setElevation(elevation); } } @Override public float getElevation(CardViewDelegate cardView) { if (useLower) { return super.getElevation(cardView); } else { return cardView.getCardView().getElevation(); } } @Override public void updatePadding(CardViewDelegate cardView) { if (useLower) { super.updatePadding(cardView); } else { 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); } } @Override public void onCompatPaddingChanged(CardViewDelegate cardView) { if (useLower) { super.onCompatPaddingChanged(cardView); } else { setMaxElevation(cardView, getMaxElevation(cardView)); } } @Override public void onPreventCornerOverlapChanged(CardViewDelegate cardView) { if (useLower) { super.onPreventCornerOverlapChanged(cardView); } else { setMaxElevation(cardView, getMaxElevation(cardView)); } } @Override public void setBackgroundColor(CardViewDelegate cardView, @Nullable ColorStateList color) { if (useLower) { super.setBackgroundColor(cardView, color); } else { getCardBackground(cardView).setColor(color); } } @Override public ColorStateList getBackgroundColor(CardViewDelegate cardView) { if (useLower) { return super.getBackgroundColor(cardView); } else { return getCardBackground(cardView).getColor(); } } private RoundRectDrawable getCardBackground(CardViewDelegate cardView) { return ((RoundRectDrawable) cardView.getCardBackground()); } }
- 到這裡,可以設定陰影顏色的CardView 就已經完成了,我們來看看效果
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" tools:context=".MainActivity"> <cn.wj.android.colorcardview.CardView android:id="@+id/cv1" android:layout_width="200dp" android:layout_height="100dp" app:cardBackgroundColor="@color/app_selector_card" app:cardElevation="8dp" app:cardPreventCornerOverlap="true" app:cardShadowColorEnd="@color/app_selector_shadow_end" app:cardShadowColorStart="@color/app_selector_shadow_start" app:cardUseCompatPadding="true" /> <android.support.v7.widget.CardView android:id="@+id/cv2" android:layout_width="200dp" android:layout_height="100dp" app:cardBackgroundColor="@color/app_selector_card" app:cardElevation="8dp" app:cardPreventCornerOverlap="true" app:cardUseCompatPadding="true" /> </LinearLayout>
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) cv1.setOnClickListener { onClick(it) } cv2.setOnClickListener { onClick(it) } } fun onClick(v: View) { v.isSelected = !v.isSelected } }

2018-10-10 14.49.23.gif
最後
- 嗯嗯,就這樣,一個能夠自定義陰影顏色的CardView 就完成了。
- 這裡給上專案地址,所有程式碼都在這裡ofollow,noindex">MyCardView 。