LinearLayout坑爹的showDividers属性

LinearLayout有个属性叫showDividers,可以用来在LinearLayout的Child之间显示一条分割线,使用起来很是方便,但是,Android啊,总是在开始时感觉有很多方便开发的东西,但使用越久,越发现坑很多。

下面用一个Demo来说明。

首先新建一个布局文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="angeldevil.me.dividerdemo.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="beginning|middle|end">
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="#22000000"
android:gravity="center_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:text="Hello World! 1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="#22000000"
android:gravity="center_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:text="Hello World! 2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="#22000000"
android:gravity="center_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:text="Hello World! 3"
android:visibility="visible"/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorPrimary"
android:gravity="center_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:text="View outside LinearLayout"/>
</LinearLayout>

很简单,就是一个LinearLayout,有3个Child,并设置showDividers="beginning|middle|bottom"显示一条分割线,为了后面说明问题,还在外层添加了一个LinearLayout,并添加了一个TextView。

divider.xml内容如下:

1
2
3
4
5
6
7
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorAccent"/>
<size android:height="4dp"/>
</shape>

为了方便查看效果,Divider高度定义为了4dp,下面是显示效果。

一切都很正常,确实很方便,但是,如果你把显示Divider的LinearLayout的最后一项的visibility设置为gone试试,下面是实际效果,测试环境Android 5.1

可以看到,底部的Divider不见了,严格来说,不是不见了,“View outside LinearLayout”与这个LinearLayout的间距还在,只是是透明的,说明系统计算高度时正确地把Divider的高度计算进去了,但却没有draw出来。

那是什么原因呢,我们跟进源码看一下。先看LinearLayout的onMeasuer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
...
}
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
...
}

可以看到,LinearLayout遍历了所有的Child,如果Visibility不为GONE就判断是否需要在它前面添加一个Divider,如果需要,就需要增加一个Divider的高度,因为for循环里判断的是是否需要显示每一个Child之前的Divider,所以end的那一个Divider需要单独判断,就是下面的hasDividerBeforeChildAt(count),从代码看,measure过程一切正常,从前面的截图也可以看到,高度计算是没有问题的。

既然是绘制不正常,那就继续看下onDraw函数,由于是在5.1下测试的,找下5.1的源码看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
protected void onDraw(Canvas canvas) {
if (mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);
} else {
drawDividersHorizontal(canvas);
}
}
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;
drawHorizontalDivider(canvas, top);
}
}
}
if (hasDividerBeforeChildAt(count)) {
final View child = getVirtualChildAt(count - 1);
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;
}
drawHorizontalDivider(canvas, bottom);
}
}

由于只是最后一条分割线画错了,所以我们只需要看drawDividersVertical的最后一个if语句,它的逻辑是,找到最后一个child,并在child.getBottom() + lp.bottomMargin下画一条线。

问题就在这里了,最后一个Child的Visibility为GONE,所以child.getBottom()结果是0结果就是这条线会画到LinearLayout的顶部,和顶部的Divider重合

如果最后一个View的marginBottom不为0,错误就更明显了,如图所示,验证了这段代码的错误逻辑(为了看到这条错误的分割线,故意把TextView的背景带上了透明度)

那解决办法是什么呢,也很简单,不要给showDividers属性设置end值,只设置beginning|middle,然后给LinearLayout最后添加个高度为0的空白View这样无论隐藏哪一个Child就都没问题了。

在Android 6.0上已经修复了这一问题,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;
drawHorizontalDivider(canvas, top);
}
}
}
if (hasDividerBeforeChildAt(count)) {
final View child = getLastNonGoneChild();
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;
}
drawHorizontalDivider(canvas, bottom);
}
}

画最后一条分割线时已经不再是画到最后一个Child的下面,而是画到最后一个Visibility不为GONE的Child的下面。