前面在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>
到这里,本文差很少结束了,因为代码全贴出来不必,因此下面提供源码,供你们参考。