緣起
平時(shí)開發(fā)中很多時(shí)候,我們需要寫這樣的布局:類似標(biāo)準(zhǔn)的設(shè)置界面,從上到下一行一行的條目,然后每個(gè)條目之間有道分隔符隔開,就像下圖這樣:

如何優(yōu)雅地實(shí)現(xiàn)
方案1:
這時(shí)你可能會(huì)想這還不簡(jiǎn)單,我在每個(gè)item view的后面都插一個(gè)額外的分隔符view(一條線),就像這樣:

雖然問(wèn)題也能解決,但不夠好、不夠優(yōu)雅。假如是在上面設(shè)置界面的case里,那我們可得有不少View需要寫在最終的xml里面。如果你的業(yè)務(wù)又需要在某些情況下隱藏某個(gè)條目,那么你還得記得隱藏某條目的時(shí)候最好把和它配對(duì)的分隔符也隱藏掉,否則2個(gè)1px的分隔符會(huì)拼在一起,變成了個(gè)2px的分隔符,仔細(xì)看會(huì)發(fā)現(xiàn)的,其實(shí)已經(jīng)算是一個(gè)小bug了,這樣的處理不僅會(huì)使邏輯變的復(fù)雜,而且很無(wú)趣。另外這種方式由于增加了不少View,即LinearLayout的children也包括了這些view,當(dāng)你想訪問(wèn)LinearLayout的children時(shí)也會(huì)帶來(lái)不少麻煩,因?yàn)樗麄円矔?huì)被算進(jìn)去的。
方案2:
其實(shí)大可不必這么麻煩,LinearLayout已經(jīng)為了這個(gè)很常見的case提供了非常優(yōu)雅的實(shí)現(xiàn),如下:

showDividers:divider顯示的位置,默認(rèn)是none,不顯示divider,其它值有beginning(第0個(gè)child前面),middle(每個(gè)child之間),end(最后一個(gè)child后面),這些值可以通過(guò)'|'組合起來(lái)使用,比如你可以指定"beginning|middle|end";
divider:具體長(zhǎng)什么樣的分隔符,是一個(gè)drawable,注意這里不能簡(jiǎn)單只給個(gè)顏色值,比如#f00或者@color/xxx這樣,drawable一定要是個(gè)有長(zhǎng)、寬概念的drawable,比如你可以純粹用一張圖片當(dāng)divider,我們這里用到的pf_linearlayout_horizonal_divider具體的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1px" />
<solid android:color="@color/mgjpf_view_divider_color" />
</shape>
同樣的我們有linearlayout_vertical_divider,用在水平的LinearLayout中,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="1px" />
<solid android:color="@color/mgjpf_view_divider_color" />
</shape>
dividerPadding:是左右或上下距離LinearLayout的邊距,有點(diǎn)margin的意思。你可以隨時(shí)在自己的demo程序里改變下這個(gè)值,然后就能立馬在預(yù)覽區(qū)看到效果,幫助你加深理解。
源碼分析
我們都知道ViewGroup由于是view容器的關(guān)系,所以它默認(rèn)是啥都不畫的,主要是它沒啥需要畫的,具體需要畫那都是某個(gè)具體子類做的事情,這個(gè)小結(jié)論參考如下ViewGroup的源碼:

說(shuō)完這個(gè)小結(jié)論,我們繼續(xù)看下LinearLayout中關(guān)于divider支持的源碼,涉及到的字段如下:

我們上面說(shuō)drawable要有長(zhǎng)寬概念的,就是對(duì)應(yīng)這里的
mDividerWidth/mDividerWidth字段;
其構(gòu)造器中有如下的源碼:

這幾個(gè)東西就是我們?cè)趚ml文件中指定的,有具體的divider drawable、showDividers、dividerPadding這些,這里我們只看下
setDividerDrawable實(shí)現(xiàn):

一般情況當(dāng)我們不指定divider的時(shí)候,第一行的if會(huì)成立,也就是說(shuō)這個(gè)方法啥也不做就返回了,但當(dāng)我們指定一個(gè)有效的divider時(shí),mDivider會(huì)被設(shè)置,并且會(huì)記錄divider的長(zhǎng)寬,這2個(gè)值在接下來(lái)布局、繪制過(guò)程中都會(huì)用到,最后注意下這里有個(gè)setWillNotDraw(divider == null);的調(diào)用,換句話說(shuō)如果沒divider,那么LinearLayout還是和ViewGroup一樣,啥都不需要畫,否則will_not_draw這個(gè)flag會(huì)被清掉,也就是需要畫東西,當(dāng)然就是需要畫具體的divider了。
接下來(lái)我們來(lái)看下,具體繪制的代碼,如下:
@Override
protected void onDraw(Canvas canvas) {
// 如果沒divider,那直接返回啥也不需要畫
if (mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);
} else {
drawDividersHorizontal(canvas);
}
}
// 在豎直的LinearLayout里畫水平的divider
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
// 非GONE的child都會(huì)被執(zhí)行這樣的操作
if (child != null && child.getVisibility() != GONE) {
// 如果它前面有的話
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 需要畫的y坐標(biāo)起始點(diǎn)
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);
}
}
/**
* Determines where to position dividers between children.
*
* @param childIndex Index of child to check for preceding divider
* @return true if there should be a divider before the child at childIndex
* @hide Pending API consideration. Currently only used internally by the system.
*/
protected boolean hasDividerBeforeChildAt(int childIndex) {
if (childIndex == getVirtualChildCount()) {
// Check whether the end divider should draw.
return (mShowDividers & SHOW_DIVIDER_END) != 0;
}
boolean allViewsAreGoneBefore = allViewsAreGoneBefore(childIndex);
if (allViewsAreGoneBefore) {
// This is the first view that's not gone, check if beginning divider is enabled.
return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
} else {
return (mShowDividers & SHOW_DIVIDER_MIDDLE) != 0;
}
}
/**
* Checks whether all (virtual) child views before the given index are gone.
*/
private boolean allViewsAreGoneBefore(int childIndex) {
for (int i = childIndex - 1; i >= 0; i--) {
View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
return false;
}
}
return true;
}
// 畫水平的divider,只需要知道y方向top坐標(biāo)即可
void drawHorizontalDivider(Canvas canvas, int top) {
mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
mDivider.draw(canvas);
}
// 同樣,畫豎直的divider,只需要知道x方向left坐標(biāo)即可
void drawVerticalDivider(Canvas canvas, int left) {
mDivider.setBounds(left, getPaddingTop() + mDividerPadding,
left + mDividerWidth, getHeight() - getPaddingBottom() - mDividerPadding);
mDivider.draw(canvas);
}
通過(guò)上面的源碼分析,我們清楚地知道這種方式要比方案1優(yōu)雅很多,因?yàn)樗恍枰腩~外的child view,而是通過(guò)draw的方式將divider畫在合適的位置,相比之下效率也會(huì)好很多。而你只需要在xml里面配置幾個(gè)標(biāo)簽即可,使用方便快捷,事半功倍。
總結(jié)
我們平時(shí)在用一些很常見的API(如findViewById)、xml寫法,這時(shí)如果能花點(diǎn)時(shí)間搞清楚內(nèi)部的工作原理,那你在實(shí)際使用中才能做到得心應(yīng)手,對(duì)自己寫的代碼也會(huì)充滿信心。