给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:///apk/res/android"
xmlns:app="http:///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.LayoutParams
、RelativeLayout.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>