LinearLayout分隔符妙用

緣起

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


標(biāo)準(zhǔn)設(shè)置界面中的分隔符

如何優(yōu)雅地實(shí)現(xiàn)

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

借助View實(shí)現(xiàn)

雖然問(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),如下:

LinearLayout在每項(xiàng)中間顯示divider

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的源碼:


ViewGroup默認(rèn)不畫任何東西

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

相關(guān)字段

我們上面說(shuō)drawable要有長(zhǎng)寬概念的,就是對(duì)應(yīng)這里的mDividerWidth/mDividerWidth字段;

其構(gòu)造器中有如下的源碼:

LinearLayout構(gòu)造器

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

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ì)充滿信心。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容