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

前言

在開(kāi)發(fā)中蚁袭,圓角和陰影效果是很常用的。實(shí)現(xiàn)的方法也很多石咬,比如通過(guò)xml自定義shape揩悄,比如通過(guò)代碼繼承drawable,還有通過(guò)第三發(fā)框架實(shí)現(xiàn)鬼悠。但是使用起來(lái)還是有些許不靈活虏束,所以我們通過(guò)自定義子view的屬性,然后通過(guò)父布局來(lái)控制子view的圓角厦章,陰影等屬性镇匀。

繼承ConstraintLayout

開(kāi)發(fā)中復(fù)雜的布局基本上都可以通過(guò)ConstraintLayout實(shí)現(xiàn),所以我們繼承ConstraintLayout實(shí)現(xiàn)一個(gè)EasyConstraintLayout能夠?yàn)樽觱iew添加圓角和陰影效果袜啃。

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

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

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

重寫(xiě)了兩個(gè)方法汗侵,我們要用這些方法實(shí)現(xiàn)子view自定義屬性的讀取,在此之前要在xml中自定義一些屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--為了方便擴(kuò)展其他layout群发,定義在外層晰韵,命名以layout_開(kāi)頭,否則lint會(huì)報(bào)紅警告-->
    <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" />
    <!--用統(tǒng)一一個(gè)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格式,這樣開(kāi)發(fā)工具會(huì)提示自定義屬性-->
    <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>

重寫(xiě)LayoutParams起愈,讀取子View自定義屬性

在EasyConstraintLayout內(nèi)部定義一個(gè)靜態(tài)類(lèi)LayoutParams繼承ConstraintLayout.LayoutParams只恨,然后在構(gòu)造方法中讀取上面自定義的屬性译仗。我們通過(guò)裁剪的方式實(shí)現(xiàn)圓角效果,因此還有要獲取子view的位置和大小官觅。

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();
    }
}

圓角和陰影實(shí)現(xiàn)原理

因?yàn)槲覀兪峭ㄟ^(guò)父布局控制子view的圓角和陰影行為纵菌,所以我們重寫(xiě)drawChild來(lái)實(shí)現(xiàn),drawChild之前休涤,先通過(guò)paint的ShadowLayer屬性把子View的陰影先畫(huà)上咱圆,這個(gè)陰影需要裁剪掉子view自身的大小位置。然后再畫(huà)子view功氨,并且裁剪圓角部分序苏,最終實(shí)現(xiàn)圓角陰影效果。
裁剪起初我們想到的是通過(guò)canvas的clipPath方法實(shí)現(xiàn)捷凄,但是發(fā)現(xiàn)會(huì)有很大的鋸齒杠览。所以改用paint的xfermode來(lái)裁剪陰影和子view。

onLayout初始化裁剪信息

在EasyConstraintLayout中初始化LayoutParamsData的paths

  @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的保存起來(lái)纵势,新增兩個(gè)屬性

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中畫(huà)陰影踱阿,裁剪出圓角

我們?cè)贓asyConstraintLayout中初始化paint,并且關(guān)閉硬件加速钦铁,然后在drawChild中實(shí)現(xiàn)陰影邏輯软舌,最終代碼如下。

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()) {//預(yù)覽模式采用裁剪
                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);
            //為解決鋸齒問(wèn)題牛曹,正式環(huán)境采用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>

最終效果如下:


顯示效果

項(xiàng)目地址

https://github.com/iamyours/EasyWidgets

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末佛点,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子黎比,更是在濱河造成了極大的恐慌超营,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阅虫,死亡現(xiàn)場(chǎng)離奇詭異演闭,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)颓帝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)米碰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人购城,你說(shuō)我怎么就攤上這事吕座。” “怎么了瘪板?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵吴趴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我侮攀,道長(zhǎng)锣枝,這世上最難降的妖魔是什么厢拭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮惊橱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘箭昵。我一直安慰自己税朴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布家制。 她就那樣靜靜地躺著正林,像睡著了一般。 火紅的嫁衣襯著肌膚如雪颤殴。 梳的紋絲不亂的頭發(fā)上觅廓,一...
    開(kāi)封第一講書(shū)人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音涵但,去河邊找鬼杈绸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛矮瘟,可吹牛的內(nèi)容都是我干的瞳脓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼澈侠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼劫侧!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起哨啃,我...
    開(kāi)封第一講書(shū)人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤烧栋,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后拳球,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體审姓,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年祝峻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了邑跪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呼猪,死狀恐怖画畅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宋距,我是刑警寧澤轴踱,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站谚赎,受9級(jí)特大地震影響淫僻,放射性物質(zhì)發(fā)生泄漏诱篷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一雳灵、第九天 我趴在偏房一處隱蔽的房頂上張望棕所。 院中可真熱鬧,春花似錦悯辙、人聲如沸琳省。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)针贬。三九已至,卻和暖如春拢蛋,著一層夾襖步出監(jiān)牢的瞬間桦他,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工谆棱, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留快压,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓垃瞧,卻偏偏與公主長(zhǎng)得像嗓节,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子皆警,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354