自定義Layout,讓子View支持圓角屬性

前言

在開發中,圓角和陰影效果是很經常使用的。實現的方法也不少,好比經過xml自定義shape,好比經過代碼繼承drawable,還有經過第三發框架實現。可是使用起來仍是有些許不靈活,因此咱們經過自定義子view的屬性,而後經過父佈局來控制子view的圓角,陰影等屬性。java

繼承ConstraintLayout

開發中複雜的佈局基本上均可以經過ConstraintLayout實現,因此咱們繼承ConstraintLayout實現一個EasyConstraintLayout可以爲子view添加圓角和陰影效果。android

public class EasyConstraintLayout extends ConstraintLayout {
    public EasyConstraintLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

   @Override
    public LinearLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }
}

重寫了兩個方法,咱們要用這些方法實現子view自定義屬性的讀取,在此以前要在xml中自定義一些屬性git

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--爲了方便擴展其餘layout,定義在外層,命名以layout_開頭,不然lint會報紅警告-->
    <attr name="layout_radius" format="dimension" />
    <attr name="layout_shadowColor" format="color" />
    <attr name="layout_shadowEvaluation" format="dimension" />
    <attr name="layout_shadowDx" format="dimension" />
    <attr name="layout_shadowDy" format="dimension" />
    <!--用統一一個EasyLayout,用於封裝讀取自定義屬性-->
    <declare-styleable name="EasyLayout">
        <attr name="layout_radius" />
        <attr name="layout_shadowColor" />
        <attr name="layout_shadowEvaluation" />
        <attr name="layout_shadowDx" />
        <attr name="layout_shadowDy" />
    </declare-styleable>
    <!--和EasyLayout屬性列表同樣,可是命名要以XXX_Layout格式,這樣開發工具會提示自定義屬性-->
    <declare-styleable name="EasyConstraintLayout_Layout">
        <attr name="layout_radius" />
        <attr name="layout_shadowColor" />
        <attr name="layout_shadowEvaluation" />
        <attr name="layout_shadowDx" />
        <attr name="layout_shadowDy" />
    </declare-styleable>
</resources>

重寫LayoutParams,讀取子View自定義屬性

EasyConstraintLayout內部定義一個靜態類LayoutParams繼承ConstraintLayout.LayoutParams,而後在構造方法中讀取上面自定義的屬性。咱們經過裁剪的方式實現圓角效果,所以還有要獲取子view的位置和大小。github

static class LayoutParams extends ConstraintLayout.LayoutParams 
                          implements EasyLayoutParams{
        private LayoutParamsData data;
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            data = new LayoutParamsData(c, attrs);
        }
        @Override
        public LayoutParamsData getData() {
            return data;
        }
    }
public interface EasyLayoutParams {
    LayoutParamsData getData();
}
public class LayoutParamsData {
    int radius;
    int shadowColor;
    int shadowDx;
    int shadowDy;
    int shadowEvaluation;

    public LayoutParamsData(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EasyLayout);
        radius = a.getDimensionPixelOffset(R.styleable.EasyLayout_layout_radius, 0);
        shadowDx = a.getDimensionPixelOffset(R.styleable.EasyLayout_layout_shadowDx, 0);
        shadowDy = a.getDimensionPixelOffset(R.styleable.EasyLayout_layout_shadowDy, 0);
        shadowColor = a.getColor(R.styleable.EasyLayout_layout_shadowColor, 0x99999999);
        shadowEvaluation = a.getDimensionPixelOffset(R.styleable.EasyLayout_layout_shadowEvaluation, 0);
        a.recycle();
    }
}

圓角和陰影實現原理

由於咱們是經過父佈局控制子view的圓角和陰影行爲,因此咱們重寫drawChild來實現,drawChild以前,先經過paintShadowLayer屬性把子View的陰影先畫上,這個陰影須要裁剪掉子view自身的大小位置。而後再畫子view,而且裁剪圓角部分,最終實現圓角陰影效果。裁剪起初咱們想到的是經過canvasclipPath方法實現,可是發現會有很大的鋸齒。因此改用paintxfermode來裁剪陰影和子view。面試

onLayout初始化裁剪信息

EasyConstraintLayout中初始化LayoutParamsDatapathscanvas

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        for (int i = 0, size = getChildCount(); i < size; i++) {
            View v = getChildAt(i);
            ViewGroup.LayoutParams lp = v.getLayoutParams();
            if(lp instanceof EasyLayoutParams){
                EasyLayoutParams elp = (EasyLayoutParams) lp;
                elp.getData().initPaths(v);
            }
        }
    }

LayoutParamsData中將裁剪陰影的path和裁剪子view的保存起來,新增兩個屬性架構

public class LayoutParamsData {
    Path widgetPath;
    Path clipPath;
    boolean needClip;
    boolean hasShadow;
  public LayoutParamsData(Context context, AttributeSet attrs) {
        …
        needClip = radius > 0;
        hasShadow = shadowEvaluation > 0;
    }
  public void initPaths(View v) {
        widgetPath = new Path();
        clipPath = new Path();
        clipPath.addRect(widgetRect, Path.Direction.CCW);
        clipPath.addRoundRect(
                widgetRect,
                radius,
                radius,
                Path.Direction.CW
        );
        widgetPath.addRoundRect(
                widgetRect,
                radius,
                radius,
                Path.Direction.CW
        );
    }
}

drawChild中畫陰影,裁剪出圓角

咱們在EasyConstraintLayout中初始化paint,而且關閉硬件加速,而後在drawChild中實現陰影邏輯,最終代碼以下。app

public class EasyConstraintLayout extends ConstraintLayout {
    private Paint shadowPaint;
    private Paint clipPaint;

    public EasyConstraintLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        shadowPaint = new Paint();
        shadowPaint.setAntiAlias(true);
        shadowPaint.setDither(true);
        shadowPaint.setFilterBitmap(true);
        shadowPaint.setStyle(Paint.Style.FILL);

        clipPaint = new Paint();
        clipPaint.setAntiAlias(true);
        clipPaint.setDither(true);
        clipPaint.setFilterBitmap(true);
        clipPaint.setStyle(Paint.Style.FILL);
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

    @Override
    public ConstraintLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        for (int i = 0, size = getChildCount(); i < size; i++) {
            View v = getChildAt(i);
            ViewGroup.LayoutParams lp = v.getLayoutParams();
            if (lp instanceof EasyLayoutParams) {
                EasyLayoutParams elp = (EasyLayoutParams) lp;
                elp.getData().initPaths(v);
            }
        }
    }

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        ViewGroup.LayoutParams lp = child.getLayoutParams();
        boolean ret = false;
        if (lp instanceof EasyLayoutParams) {
            EasyLayoutParams elp = (EasyLayoutParams) lp;
            LayoutParamsData data = elp.getData();
            if (isInEditMode()) {//預覽模式採用裁剪
                canvas.save();
                canvas.clipPath(data.widgetPath);
                ret = super.drawChild(canvas, child, drawingTime);
                canvas.restore();
                return ret;
            }
            if (!data.hasShadow && !data.needClip)
                return super.drawChild(canvas, child, drawingTime);
            //爲解決鋸齒問題,正式環境採用xfermode
            if (data.hasShadow) {
                int count = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
                shadowPaint.setShadowLayer(data.shadowEvaluation, data.shadowDx, data.shadowDy, data.shadowColor);
                shadowPaint.setColor(data.shadowColor);
                canvas.drawPath(data.widgetPath, shadowPaint);
                shadowPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
                shadowPaint.setColor(Color.WHITE);
                canvas.drawPath(data.widgetPath, shadowPaint);
                shadowPaint.setXfermode(null);
                canvas.restoreToCount(count);

            }
            if (data.needClip) {
                int count = canvas.saveLayer(child.getLeft(), child.getTop(), child.getRight(), child.getBottom(), null, Canvas.ALL_SAVE_FLAG);
                ret = super.drawChild(canvas, child, drawingTime);
                clipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
                clipPaint.setColor(Color.WHITE);
                canvas.drawPath(data.clipPath, clipPaint);
                clipPaint.setXfermode(null);
                canvas.restoreToCount(count);
            }
        }
        return ret;
    }

    static class LayoutParams extends ConstraintLayout.LayoutParams implements EasyLayoutParams {

        private LayoutParamsData data;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            data = new LayoutParamsData(c, attrs);
        }

        @Override
        public LayoutParamsData getData() {
            return data;
        }
    }
}

使用方法

<?xml version="1.0" encoding="utf-8"?>
<io.github.iamyours.easylayout.EasyConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <View
        android:id="@+id/v_back"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:layout_margin="10dp"
        android:background="#fff"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_radius="4dp"
        app:layout_shadowColor="#3ccc"
        app:layout_shadowEvaluation="15dp" />

    <ImageView
        android:id="@+id/iv_head"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginLeft="10dp"
        android:background="#eee"
        app:layout_constraintBottom_toBottomOf="@id/v_back"
        app:layout_constraintLeft_toLeftOf="@id/v_back"
        app:layout_constraintTop_toTopOf="@id/v_back"
        app:layout_radius="40dp"
        app:layout_shadowColor="#5f00"
        app:layout_shadowEvaluation="8dp" />

    <View
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_marginTop="30dp"
        android:background="#ccc"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/v_back"
        app:layout_radius="30dp"
        app:layout_shadowColor="#8f0f"
        app:layout_shadowDx="4dp"
        app:layout_shadowDy="4dp"
        app:layout_shadowEvaluation="10dp" />
</io.github.iamyours.easylayout.EasyConstraintLayout>

最終效果以下:框架

項目地址: https://github.com/iamyours/E...ide

讀者福利分享

Android開發資料+面試架構資料 免費分享 點擊連接 便可領取

《Android架構師必備學習資源免費領取(架構視頻+面試專題文檔+學習筆記)》

相關文章
相關標籤/搜索