CardView陰影自定義--ShadowLayout

前面在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

  1. initStatic 這個方法是在CardView建立CardViewImpl對象的靜態代碼塊中調用的。
  2. initialize 這個方法實在CardView的構造方法中調用,主要邏輯是CardView將他被代理過程當中須要用到的對象傳入代理對象中,並初始化,即建立RoundRectDrawableWithShadow/RoundRectDrawable,設置陰影相關參數。

下面分別對比下CardViewBaseImplCardViewApi17ImplCardViewApi21Impl之間這兩個方法的實現到底有何不一樣。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是在RoundRectDrawableWithShadowdraw方法裏面調用的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的源碼修改。固然,兩種方法都須要本身實現CardViewImplide

實現自定義陰影

先來看看對比圖:
CardView和ShadowLayout對比圖
具體實現思路就是經過自定義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只實現了ShadowLayoutBaseImplShadowLayoutApi17Impl。它們的區別只是在於對於圓角的實現,由於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>

到這裏,本文差很少結束了,因爲代碼全貼出來不必,因此下面提供源碼,供你們參考。

源碼地址:https://github.com/qinzhen308...

相關文章
相關標籤/搜索