给LinearLayout加上花式分割线

前言

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

惯例上图

android tablayout 取消分割线 linearlayout分割线_ide


最终通过继承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>