给LinearLayout加上花式分割线

来源:互联网 发布:淘宝上怎样删除评价 编辑:程序博客网 时间:2024/04/29 21:57

给LinearLayout加上花式分割线

前言

写安卓的同学们应该都知道LinearLayout有一个分割线的功能,可以在子View间添加分割线或是给整个LinearLayout上下加上分割线,同时对于分割线的样式,可以通过自定义drawable的方式来实现,灵活度很高,但这种方式也让开发人员的编码变得非常痛苦。写安卓也有一段时间了,你要问我安卓开发的过程中,最不愿意面对的事情是什么,我想说就是打开我的drawable文件夹或是打开layout文件夹了… 对于一个简单的横线,我还是不太愿意定义一个drawable文件来解决这个问题,毕竟图多了不好找,我又不是HashMap! 所以今天来讨论下怎么去掉这个drawable文件的问题。

惯例上图

就问你花不花
最终通过继承LinearLayout的方式干掉了drawable文件,直接通过属性指定即可,同时还顺带做了分割线颜色尺寸位置的控制,每条分割线都可以单独控制,是不是很带劲

使用方式

自定义ViewGroup(DividerLayout)是LinearLayout的子类,它可以为每一个直接子View提供在其上下两侧绘制分割线的功能,你可以直接在每个子View中声明app:divider_top="true"或者app:divider_bottom="true" 来绘制某个子View的上下分割线
同时你可以通过:
app:divider_size="2px"
app:divider_color="@color/colorPrimary"
app:divider_padding_left="48dp"
app:divider_padding_right="48dp"
这四个选项来控制每一条分割线的颜色大小及padding。值得一说的是,如果这些属性被声明在DividerLayout中,这些属性将被作为默认属性应用到每一个子view中 但子view可以再次声明相关属性达到覆盖的效果

除了可以在子View上下添加分割线,整个DividerLayout也是支持在自己的上面或下面绘制分割线的 使用方式同子View一样依然是app:divider_top="true"或者app:divider_bottom="true",只不过把他写到DividerLayout中就可以了。

这样一来,整个DividerLayout就实现了全部的LinearLayout提供的分割线功能,同时,提供了更加细化和简单的控制方式,所以,请尽情享用它吧!

贴一下上面那张图的xml代码

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:background="#f0f0f0"    android:layout_height="match_parent">    <com.congxiaoyao.xber_admin.widget.XberDividerLayout        app:divider_color="@color/colorPrimary"        app:divider_size="2px"        app:divider_bottom="true"        android:layout_marginTop="8dp"        android:orientation="vertical"        android:background="#ffffff"        android:layout_width="match_parent"        android:layout_height="wrap_content">        <TextView            android:layout_width="match_parent"            android:layout_height="40dp"            android:textSize="17sp"            android:gravity="center"            app:divider_top="true"            android:text="第一项" />        <TextView            app:divider_top="true"            app:divider_padding_left="24dp"            app:divider_padding_right="24dp"            app:divider_color="#b44bb8"            android:layout_width="match_parent"            android:layout_height="40dp"            android:textSize="17sp"            android:gravity="center"            android:text="第二项" />        <TextView            app:divider_top="true"            app:divider_padding_left="48dp"            app:divider_padding_right="48dp"            app:divider_color="#008d58"            android:layout_width="match_parent"            android:layout_height="40dp"            android:textSize="17sp"            android:gravity="center"            android:text="第三项" />        <TextView            app:divider_top="true"            app:divider_padding_left="72dp"            app:divider_padding_right="72dp"            app:divider_color="#efb11f"            android:layout_width="match_parent"            android:layout_height="40dp"            android:textSize="17sp"            android:gravity="center"            android:text="第四项" />    </com.congxiaoyao.xber_admin.widget.XberDividerLayout></LinearLayout>

实现方式

前文也提到了,DividerLayout是通过继承LinearLayout的方式来实现的。毕竟为了加个分割线,重写一遍LinearLayout就太伤了,所以这里稍微hack下,只需要单纯的添加一些绘图代码及布局参数控制代码就好了。其实我们面对的技术问题只有两点:

  • 如何获取在子View中定义的属性
  • 如何在分割线存在的情况下 为子View排布新的位置(要为分割线空出位置)

关于第一点 大家可以参考这篇文章,如果大家了解,可以跳过了,这里简单介绍一下。
其实秘密就在于每一个子View的LayoutParam参数。作为一个ViewGroup,系统在为其每一个子View生成布局参数的时候,给予了ViewGroup一次获取子View属性的机会,也就是说,在生成子View的LayoutParams的时候,ViewGroup还有一次访问AttributeSet的机会,而且这个AttributeSet是子View的AttributeSet!是不是突然明白了?是吧,我也是。所以,我们可以定义一堆属性并且在生成LayoutParam的时候解析他 看代码

    public class LayoutParams extends LinearLayout.LayoutParams {        private int dividerColor;        private int dividerPaddingLeft;        private int dividerPaddingRight;        private int dividerSize;        private boolean dividerTop;        private boolean dividerBottom;        public LayoutParams(Context c, AttributeSet attrs) {            super(c, attrs);            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.XberDividerLayout);            dividerColor = a.getColor(R.styleable.XberDividerLayout_divider_color,                    XberDividerLayout.this.dividerColor);            dividerPaddingLeft = a.getDimensionPixelSize(R.styleable                            .XberDividerLayout_divider_padding_left,                    XberDividerLayout.this.dividerPaddingLeft);            dividerPaddingRight = a.getDimensionPixelSize(R.styleable                            .XberDividerLayout_divider_padding_right,                    XberDividerLayout.this.dividerPaddingRight);            ......            ......            a.recycle();        }        //下面还有几个构造函数 但是可以不关心她        ...   }

这里定义了一个内部类LayoutParams,可能你会经常看到FrameLayout.LayoutParamsRelativeLayout.LayoutParams 等等,其实就是这个意思。但单纯的定义是没办法让系统把这个布局参数应用给子View的,所以还需要将我们自定义的这个LayoutParams告诉系统,也就是覆写如下方法把我们自己的LayoutParam返回出去。如果这里看的不是很明白可以去看下刚刚提到的那篇文章

    @Override    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {        return new LayoutParams(lp);    }    @Override    public LayoutParams generateLayoutParams(AttributeSet attrs) {        return new LayoutParams(getContext(), attrs);    }    @Override    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {        return p instanceof LayoutParams;    }

这样我们在画分割线的时候就可以遍历每一个子View,通过LayoutParam参数拿到相关的属性了。

好第一个问题解决了,那位置怎么控制呢,难道要重写onLayout或onMeasure方法吗?如果这么办的话,那就基本上是把LinearLayout重写一遍了,所以在这个地方,我选择抖一波机灵,直接把分割线的尺寸当做margin加进布局参数,让系统自动的帮我们测量及布局,所以如果是上分割线就加topMargin,下分割线就加bottomMargin,简单且无害,绿色环保。

那么构造函数再加两句

        public LayoutParams(Context c, AttributeSet attrs) {            super(c, attrs);            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.XberDividerLayout);            ......            ......            dividerSize = a.getDimensionPixelSize(R.styleable                    .XberDividerLayout_divider_size, XberDividerLayout.this.dividerSize);            dividerTop = a.getBoolean(R.styleable.XberDividerLayout_divider_top, false);            if (dividerTop) {                topMargin += dividerSize;            }            dividerBottom = a.getBoolean(R.styleable.XberDividerLayout_divider_bottom, false);            if (dividerBottom) {                bottomMargin += dividerSize;            }            a.recycle();        }

所以到这就是所有核心的东西了,有没有比你想象的要简单一点。。。不管怎么说,管用就行。只是还有最后一点,就是要为整个DividerLayout添加上下的分割线,所以这里还是存在一个位置排布的问题,得把上下分割线的位置空出来。之前给子view添加margin的方式到也可以解决这个问题,但是还是有点复杂,仔细想想,其实还有一个特性我们没有用到,那就是LinearLayout本身对分割线的支持啊! 它本身就是支持添加上线分割线的,所以我们直接通过代码调用相关方法就好了 看代码!

public XberDividerLayout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.XberDividerLayout);        dividerSize = a.getDimensionPixelSize(R.styleable.XberDividerLayout_divider_size, 0);        dividerColor = a.getColor(R.styleable.XberDividerLayout_divider_color, Color.BLACK);        dividerPaddingLeft = a.getDimensionPixelSize(R.styleable                .XberDividerLayout_divider_padding_left, 0);        dividerPaddingRight = a.getDimensionPixelSize(R.styleable                .XberDividerLayout_divider_padding_right, 0);        boolean top = a.getBoolean(R.styleable.XberDividerLayout_divider_top, false);        boolean bottom = a.getBoolean(R.styleable.XberDividerLayout_divider_bottom, false);        a.recycle();        //从这开始看!        int showDivider = SHOW_DIVIDER_NONE;        if(top) showDivider = showDivider | SHOW_DIVIDER_BEGINNING;        if(bottom) showDivider = showDivider | SHOW_DIVIDER_END;        if (showDivider != SHOW_DIVIDER_NONE) {            //注意这里,通过代码来设置开启上下分割线            setShowDividers(showDivider);            //通过代码生成一个ColorDrawable并设置进去            setDividerDrawable(new ColorDrawable(dividerColor){                @Override                public int getIntrinsicHeight() {                    return dividerSize;                }                @Override                public void setBounds(Rect bounds) {                    super.setBounds(bounds);                    bounds.left += dividerPaddingLeft;                    bounds.right -= dividerPaddingRight;                }            });        }    }

你可能注意到了,我们并没有直接传ColorDrawable进去,而是覆写了他两个方法。关于getIntrinsicHeight方法,在系统进行measure和layout的时候会根据此方法的返回值来空出相应的位置画分割线
在LinearLayout的 measureVertical 方法中 有如下语句

if (hasDividerBeforeChildAt(i)) {    //mTotalLength表示整个linearLayout的高度 在遍历测量每一个子view的过程中,将分割线的高度也算进了总高度里    mTotalLength += mDividerHeight;}

这里的 mDividerHeight 就是在这个方法里初始化的

 public void setDividerDrawable(Drawable divider) {        if (divider == mDivider) {            return;        }        mDivider = divider;        if (divider != null) {            mDividerWidth = divider.getIntrinsicWidth();            //看这句!            mDividerHeight = divider.getIntrinsicHeight();        } else {            mDividerWidth = 0;            mDividerHeight = 0;        }        setWillNotDraw(divider == null);        requestLayout();    }

所以我们直接覆盖getIntrinsicHeight方法,返回布局中设置的高度即可达到让父类帮我们测量尺寸的目的。同理在onLayout方法中,也是根据mDividerHeight 这个变量来控制view的偏移的,代码不再贴了。
还有一点就是上面又覆写了setBounds方法,是因为我们是支持左右padding的,而系统只支持一个padding,但这并不代表我们没法hack他,仔细观察LinearLayout的onDraw方法,就会看到如下代码

    void drawHorizontalDivider(Canvas canvas, int top) {        mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,                getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);        mDivider.draw(canvas);    }

在每次调用drawable的draw方法前,系统都会为这个drawable设置一次边界,所以我们在这里动一下手脚把我们自己的padding加进去就可以了,所以为了padding能够正常工作,请不要使用任何LinearLayout本身的divider设置方法,否则上下分割线就不起作用了。

收工啦

好了 到这就全部结束了,以后有各种分割线的需求,都不用再写个View放那了。暂时不支持横向布局,我想百分之九十九都不会在横向布局里加分割线吧。如果需要,直走左转LinearLayout在门口等你,文末我会把代码全部附上,小玩具我就不做gradle依赖了,大家有需要自己把代码粘走吧~

以上!

package com.congxiaoyao.xber_admin.widget;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Rect;import android.graphics.drawable.ColorDrawable;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;import android.widget.LinearLayout;import com.congxiaoyao.xber_admin.R;/** * Created by congxiaoyao on 2017/4/3. */public class XberDividerLayout extends LinearLayout {    private int dividerSize;    private int dividerColor;    private int dividerPaddingLeft;    private int dividerPaddingRight;    private ColorDrawable dividerDrawable = new ColorDrawable();    public XberDividerLayout(Context context) {        this(context, null);    }    public XberDividerLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public XberDividerLayout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.XberDividerLayout);        dividerSize = a.getDimensionPixelSize(R.styleable.XberDividerLayout_divider_size, 0);        dividerColor = a.getColor(R.styleable.XberDividerLayout_divider_color, Color.BLACK);        dividerPaddingLeft = a.getDimensionPixelSize(R.styleable                .XberDividerLayout_divider_padding_left, 0);        dividerPaddingRight = a.getDimensionPixelSize(R.styleable                .XberDividerLayout_divider_padding_right, 0);        boolean top = a.getBoolean(R.styleable.XberDividerLayout_divider_top, false);        boolean bottom = a.getBoolean(R.styleable.XberDividerLayout_divider_bottom, false);        a.recycle();        int showDivider = SHOW_DIVIDER_NONE;        if(top) showDivider = showDivider | SHOW_DIVIDER_BEGINNING;        if(bottom) showDivider = showDivider | SHOW_DIVIDER_END;        if (showDivider != SHOW_DIVIDER_NONE) {            setShowDividers(showDivider);            setDividerDrawable(new ColorDrawable(dividerColor){                @Override                public int getIntrinsicHeight() {                    return dividerSize;                }                @Override                public void setBounds(Rect bounds) {                    super.setBounds(bounds);                    bounds.left += dividerPaddingLeft;                    bounds.right -= dividerPaddingRight;                }            });        }        setWillNotDraw(false);    }    @Override    protected void onDraw(Canvas canvas) {        if (getOrientation() == HORIZONTAL) {            throw new RuntimeException("暂不支持横向布局");        }        super.onDraw(canvas);        drawDividersVertical(canvas);    }    private void drawDividersVertical(Canvas canvas) {        final int count = getChildCount();        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            if (child != null && child.getVisibility() != GONE) {                ViewGroup.LayoutParams layoutParams = child.getLayoutParams();                if (hasDividerBeforeChild(layoutParams)) {                    final LayoutParams lp = (LayoutParams) layoutParams;                    final int top = child.getTop() - lp.dividerSize;                    drawHorizontalDivider(canvas, top, lp);                }                if (hasDividerAfterChild(layoutParams)) {                    final LayoutParams lp = (LayoutParams) layoutParams;                    final int top = child.getBottom();                    drawHorizontalDivider(canvas, top, lp);                }            }        }    }    private boolean hasDividerBeforeChild(ViewGroup.LayoutParams lp) {        if (!(lp instanceof LayoutParams)) {            return false;        }        LayoutParams layoutParams = (LayoutParams) lp;        return layoutParams.dividerSize > 0 && layoutParams.dividerTop;    }    private boolean hasDividerAfterChild(ViewGroup.LayoutParams lp) {        if (!(lp instanceof LayoutParams)) {            return false;        }        LayoutParams layoutParams = (LayoutParams) lp;        return layoutParams.dividerSize > 0 && layoutParams.dividerBottom;    }    private void drawHorizontalDivider(Canvas canvas, int top, LayoutParams lp) {        dividerDrawable.setColor(lp.dividerColor);        dividerDrawable.setBounds(getPaddingLeft() + lp.dividerPaddingLeft, top,                getWidth() - getPaddingRight() - lp.dividerPaddingRight, top + lp.dividerSize);        dividerDrawable.draw(canvas);    }    @Override    protected LayoutParams generateDefaultLayoutParams() {        if (getOrientation() == HORIZONTAL) {            throw new RuntimeException("暂不支持横向布局");        }        return new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,                LinearLayout.LayoutParams.WRAP_CONTENT);    }    @Override    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {        return new LayoutParams(lp);    }    @Override    public LayoutParams generateLayoutParams(AttributeSet attrs) {        return new LayoutParams(getContext(), attrs);    }    @Override    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {        return p instanceof LayoutParams;    }    public class LayoutParams extends LinearLayout.LayoutParams {        private int dividerColor;        private int dividerPaddingLeft;        private int dividerPaddingRight;        private int dividerSize;        private boolean dividerTop;        private boolean dividerBottom;        public LayoutParams(Context c, AttributeSet attrs) {            super(c, attrs);            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.XberDividerLayout);            dividerColor = a.getColor(R.styleable.XberDividerLayout_divider_color,                    XberDividerLayout.this.dividerColor);            dividerPaddingLeft = a.getDimensionPixelSize(R.styleable                            .XberDividerLayout_divider_padding_left,                    XberDividerLayout.this.dividerPaddingLeft);            dividerPaddingRight = a.getDimensionPixelSize(R.styleable                            .XberDividerLayout_divider_padding_right,                    XberDividerLayout.this.dividerPaddingRight);            dividerSize = a.getDimensionPixelSize(R.styleable                    .XberDividerLayout_divider_size, XberDividerLayout.this.dividerSize);            dividerTop = a.getBoolean(R.styleable.XberDividerLayout_divider_top, false);            if (dividerTop) {                topMargin += dividerSize;            }            dividerBottom = a.getBoolean(R.styleable.XberDividerLayout_divider_bottom, false);            if (dividerBottom) {                bottomMargin += dividerSize;            }            a.recycle();        }        public LayoutParams(int width, int height) {            super(width, height);        }        public LayoutParams(int width, int height, float weight) {            super(width, height, weight);        }        public LayoutParams(ViewGroup.LayoutParams p) {            super(p);        }        public LayoutParams(LinearLayout.LayoutParams source) {            super(source);        }    }}

res/values/attrs.xml

    <declare-styleable name="XberDividerLayout" >        <attr name="divider_size" format="dimension"/>        <attr name="divider_color" format="color" />        <attr name="divider_padding_left" format="dimension"/>        <attr name="divider_padding_right" format="dimension"/>        <attr name="divider_top" format="boolean" />        <attr name="divider_bottom" format="boolean" />        <attr name="enable_header" format="boolean"/>        <attr name="enable_footer" format="boolean" />    </declare-styleable>
0 0
原创粉丝点击