前面在CardView簡析中簡要講了CardView的基本屬性和其實現邏輯,本文基於CardView的實現原理,並基於CardView的源碼進行修改,實現對CardView陰影效果的修改。
首先須要明確的一點,CardView的陰影效果是沒提供api進行修改的,只能經過cardElevation改變投影深度。android
CardView是經過CardViewImpl來靜態代理圓角和陰影繪製以及受他們影響的padding處理邏輯的,先來看看這段代碼:git
public class CardView extends FrameLayout { ... private static final CardViewImpl IMPL; static { if (Build.VERSION.SDK_INT >= 21) { IMPL = new CardViewApi21Impl(); } else if (Build.VERSION.SDK_INT >= 17) { IMPL = new CardViewApi17Impl(); } else { IMPL = new CardViewBaseImpl(); } IMPL.initStatic(); } ... }
從代碼中咱們瞭解到這代理接口是在CardView的類加載的時候初始化的,而且是根據安卓版本不一樣建立了其不一樣的實現類。
那接下來看看這個接口到底提供了哪些方法:github
interface CardViewImpl { //初始化方法,裏面邏輯主要是建立Drawable並設置爲CardView的背景 void initialize(CardViewDelegate cardView, Context context, ColorStateList backgroundColor, float radius, float elevation, float maxElevation); //設置圓角半徑 void setRadius(CardViewDelegate cardView, float radius); float getRadius(CardViewDelegate cardView); //設置陰影高度 void setElevation(CardViewDelegate cardView, float elevation); float getElevation(CardViewDelegate cardView); //全局內容的初始化,主要是針對不一樣安卓版本下的RoundRectDrawableWithShadow.sRoundRectHelper進行初始化 void initStatic(); void setMaxElevation(CardViewDelegate cardView, float maxElevation); float getMaxElevation(CardViewDelegate cardView); float getMinWidth(CardViewDelegate cardView); float getMinHeight(CardViewDelegate cardView); void updatePadding(CardViewDelegate cardView); void onCompatPaddingChanged(CardViewDelegate cardView); void onPreventCornerOverlapChanged(CardViewDelegate cardView); void setBackgroundColor(CardViewDelegate cardView, @Nullable ColorStateList color); ColorStateList getBackgroundColor(CardViewDelegate cardView); }
這裏主要關注如下兩個方法:canvas
下面分別對比下CardViewBaseImpl、CardViewApi17Impl和CardViewApi21Impl之間這兩個方法的實現到底有何不一樣。segmentfault
class CardViewBaseImpl implements CardViewImpl { ... @Override public void initStatic() { // Draws a round rect using 7 draw operations. This is faster than using // canvas.drawRoundRect before JBMR1 because API 11-16 used alpha mask textures to draw // shapes. 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; mCornerRect.set(-roundedCornerRadius, -roundedCornerRadius, roundedCornerRadius, roundedCornerRadius); int saved = canvas.save(); canvas.translate(bounds.left + roundedCornerRadius, bounds.top + roundedCornerRadius); canvas.drawArc(mCornerRect, 180, 90, true, paint); canvas.translate(innerWidth, 0); canvas.rotate(90); canvas.drawArc(mCornerRect, 180, 90, true, paint); canvas.translate(innerHeight, 0); canvas.rotate(90); canvas.drawArc(mCornerRect, 180, 90, true, paint); canvas.translate(innerWidth, 0); canvas.rotate(90); canvas.drawArc(mCornerRect, 180, 90, true, paint); canvas.restoreToCount(saved); //draw top and bottom pieces 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); } // center canvas.drawRect(bounds.left, bounds.top + cornerRadius, bounds.right, bounds.bottom - cornerRadius , paint); } }; } @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); } ... }
@RequiresApi(17) class CardViewApi17Impl extends CardViewBaseImpl { @Override 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); } }; } }
@RequiresApi(21) class CardViewApi21Impl implements CardViewImpl { ... @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); } @Override public void initStatic() { } ... }
由上面的代碼得知,安卓L以上,是以RoundRectDrawable來實現圓角,View.setElevation來提供陰影,Elevation在安卓5.0以上的全部View中都支持,直接由RenderNode渲染陰影。而在L如下,就只能經過Canvas繪製出陰影,因此RoundRectDrawableWithShadow就是爲了知足圓角和陰影而出現的。initStatic中對RoundRectDrawableWithShadow.sRoundRectHelper的實現不一樣,drawRoundRect是在RoundRectDrawableWithShadow的draw方法裏面調用的api
class RoundRectDrawableWithShadow extends Drawable { @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); } }
由此得知,CardView的陰影效果是應用層的統一效果,並未開放擴展和修改。那麼既然分析清楚緣由了,且清楚了CardView是在什麼地方實現陰影的,那咱們就本身能夠進行修改。有兩種方式能夠修改這個效果,一種是經過反射替換CardView.IMPL,另外一種方法,就是直接參照CardView的源碼修改。固然,兩種方法都須要本身實現CardViewImpl。ide
先來看看對比圖:
具體實現思路就是經過自定義CardViewImpl從而改變CardView陰影實現邏輯。下面我就闡述下我本身的實現簡要過程。
我一樣是經過代理模式,來實現對CardView的繪製管理,ShadowLayoutImpl對應CardViewImplui
public interface ShadowLayoutImpl { void initialize(ShadowLayoutDelegate cardView, Context context, ColorStateList backgroundColor, float radius,int shadowStartColor,int shadowEndColor, float elevation, float maxElevation); void setRadius(ShadowLayoutDelegate cardView, float radius); float getRadius(ShadowLayoutDelegate cardView); void setElevation(ShadowLayoutDelegate cardView, float elevation); float getElevation(ShadowLayoutDelegate cardView); void initStatic(); void setMaxElevation(ShadowLayoutDelegate cardView, float maxElevation); float getMaxElevation(ShadowLayoutDelegate cardView); float getMinWidth(ShadowLayoutDelegate cardView); float getMinHeight(ShadowLayoutDelegate cardView); void updatePadding(ShadowLayoutDelegate cardView); void onCompatPaddingChanged(ShadowLayoutDelegate cardView); void onPreventCornerOverlapChanged(ShadowLayoutDelegate cardView); void setBackgroundColor(ShadowLayoutDelegate cardView, @Nullable ColorStateList color); ColorStateList getBackgroundColor(ShadowLayoutDelegate cardView); }
因爲咱們不須要安卓L或CardView提供的陰影效果。因此ShadowLayoutImpl只實現了ShadowLayoutBaseImpl和ShadowLayoutApi17Impl。它們的區別只是在於對於圓角的實現,由於Canvas.drawRoundRect(bounds, cornerRadius, cornerRadius, paint) 只在SDK>=17纔有。它們都一樣使用了RoundRectDrawableWithShadow,下面貼出實現陰影的繪製邏輯:this
public class RoundRectDrawableWithShadow extends Drawable { ... @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); } private void drawShadow(Canvas canvas) { final float edgeShadowTop = -mCornerRadius - mShadowSize; // final float inset = 0; final float inset = mCornerRadius + mInsetShadow ; final boolean drawHorizontalEdges = mCardBounds.width() - 2 * inset > 0; final boolean drawVerticalEdges = mCardBounds.height() - 2 * inset > 0; int saved=0; // LT 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 , 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); } 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)*.2f; 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, .1f, 1f}, Shader.TileMode.CLAMP)); mEdgeShadowPaint.setAntiAlias(false); } 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(); } ... }
自定義參數和CardView差很少,因此使用方法也差很少。
attrs.xml:spa
<declare-styleable name="ShadowLayout"> <attr name="android_minWidth" format="dimension" /> <attr name="android_minHeight" format="dimension" /> <attr name="cardCornerRadius" format="dimension" /> <attr name="cardElevation" format="dimension" /> <attr name="cardMaxElevation" format="dimension" /> <attr name="cardBackgroundColor" format="color" /> <attr name="cardPreventCornerOverlap" format="boolean" /> <attr name="cardUseCompatPadding" format="boolean" /> <attr name="contentPadding" format="dimension" /> <attr name="contentPaddingBottom" format="dimension" /> <attr name="contentPaddingLeft" format="dimension" /> <attr name="contentPaddingRight" format="dimension" /> <attr name="contentPaddingTop" format="dimension" /> <attr name="cardShadowStartColor" format="color" /> <attr name="cardShadowEndColor" format="color" /> </declare-styleable>
到這裏,本文差很少結束了,因爲代碼全貼出來不必,因此下面提供源碼,供你們參考。