教你步步為營掌握Android中的自定義ViewGroup

ybsoft 8年前發布 | 15K 次閱讀 安卓開發 Android開發 移動開發

本篇是《教你步步為營掌握自定義View》一文的姊妹篇。自定義ViewGroup的文章很多,但都有一個缺點,沒有回應用戶關切,比如我在讀那些文章時,就很想知道,自定義的ViewGroup如何使用layout_gravity?在onMeasure中,自定義的ViewGroup會將所有子View的尺寸加起來設置成自己的尺寸,如果超過了自定義ViewGroup的parent限定的尺寸怎么辦?而且onMeasure中,ViewGroup給 每一個 子View設置的MeasureSpec中的寬高都是它的parent給它的寬高,為什么不是每measure一個子View,就把它的尺寸減去后再去measure下一個子View呢?如果一個ViewGroup把一個子View layout到了自己掌控的屏幕區域之外,這個View還怎么發揮作用呢?如果你也有類似的疑問,這篇文章也許能讓你有茅塞頓開之感。

一、自定義ViewGroup必須清楚的基本原理

在學習一個技術點的時候,我會先搞清楚它的基本原理,然后再動手編碼,因為我希望對自己寫的每一行代碼最終將會怎樣執行有精準的把握。否則,我寫代碼時就沒有底氣,就像在棉花堆上走路,每一步都會心里發虛。學習自定義ViewGroup當然也不例外。下面,我們就一起看看自定義ViewGroup的原理吧。

1、自定義ViewGroup本質上就干一件事-layout

通過上篇文章,我們知道ViewGroup是一個組合View,它與普通的基本View(只要不是ViewGroup,都是基本View)最大的區別在于,它可以容納其他View,這些View既可以是基本View,也可以ViewGroup,但是在我們的ViewGroup眼中,不管是View還是ViewGroup,它們都抽象成了一個普通的View,ViewGroup的 最最根本的職責 就是,在自己內部,給它們每一個人找一個合適的位置,也就是調用它們的如下方法:

public void layout(int left, int top, int right, int bottom)

如下圖所示:

ViewGroup-demo.png

這個方法,可謂是一箭雙雕,既確定了子View的位置,也確定了子View的大小,請注意,這個大小是由我們的ViewGroup最后決定的分給該子View的屏幕區域大小,一般情況下,作為老大哥,我們的ViewGroup在設定這個大小時,會考慮子View的自身要求的,也就是它們measured的大小(getMeasuredWidth , getMeasuredHeight),通常最后給每個子View設定的大小就是它們所要求的大小,但這不是絕對的。假如有一個二愣子性格的ViewGroup,它宣稱:“我所有的子View的大小都必須是30*30的尺寸!”,這種SB的ViewGroup在調用每個子View的layout方法時,通過讓bottom-top=right-left=30,就把所有的子View最后占據的屏幕區域設定為30*30了,不管各個子View所要求的大小是多少,此時都沒有任何用處了。當然,除了有特殊需求,我相信沒人愿意用這種ViewGroup的,這里我們可以知道,我們自定義ViewGroup,大體上有兩條路可選,一條就是讓這個ViewGroup滿足我們開發中的特定需求,這個時候,你可以隨心所欲地去定義ViewGroup,反正我也只是自己用,不打算給別人用的。另一條就是自定義一個ViewGroup,提供給更多的人使用,這個時候,你就要遵守一些基本的規矩,讓你的ViewGroup符合使用者的使用習慣和期望,這樣大家才能愿意用你的ViewGroup。那么使用者使用一個ViewGroup最基本的期望是什么?我想,應該是使用者放入這個ViewGroup中的子View layout出來的尺寸和每個子View measured的尺寸相符。只有這樣,才能確保使用者的每個子View順利完成自己的交互任務。

對于上面的圖,有兩點非常容易讓人產生誤解,需要解釋一下:

  • 關于left、right、top、bottom。它們都是坐標值,既然是坐標值,就要明確坐標系,這個坐標系是什么?我們知道,這些值都是ViewGroup設定的,那么,這個坐標系自然也是由ViewGroup決定的了。沒錯,這個坐標系就是以ViewGroup左上角為原點,向右x,向下y構建起來的。進一步我們又想問,ViewGroup的左上角又在哪里呢?我們知道,在ViewGroup的parent(也是ViewGroup)眼中,我們的ViewGroup就是一個普通的View,parent也會調用我們的ViewGroup的如下方法:
    //注意,這個layout方法是ViewGroup的parent在layout我們的ViewGroup,
    //不要和我們的ViewGroup layout自己的子View搞混了。
    public void layout(int left, int top, int right, int bottom)
    此時,我們的ViewGroup的左上角,就是在parent的坐標系內的點(left,top)。好奇的你可能又問,假如我們的ViewGroup沒有parent,它的左上角在屏幕上的位置又該如何確定?上篇文章中,我們提到,系統控制的Window都有一個DecorView,我們所能創建的View也好,ViewGroup也好,都是它的兒子、孫子、重孫、重重孫......,所以不用擔心我們的ViewGroup沒有parent,至于DecorView左上角在屏幕上的位置,是由系統幫我們決定的,我們不用操那么多心。
    由此我們看到,Google創建的這一套坐標系統非常的高效,只要確定DecorView左上角在屏幕上的位置,那么,所有的View在屏幕上的相對位置都可以精準地確定。
  • 第二點就是上圖中代表ViewGroup的那個方框。這個方框是什么意思,是代表ViewGroup的大小嗎?如果是的話,這個大小是不是ViewGroup在onMeasure方法中設定的各個子View大小的和?正確的答案是,這個方框是ViewGroup的parent在layout我們的ViewGroup時,給ViewGroup設定的大小,parent調用我們的ViewGroup的如下layout方法:
    //注意,這個layout方法是ViewGroup的parent在layout我們的ViewGroup,
    //不要和我們的ViewGroup layout自己的子View搞混了。
    public void layout(int left, int top, int right, int bottom)
    上圖中,代表ViewGroup的方框的寬是上述方法中的right-left,方框的高是bottom-top。我們一般將這個寬高稱為 availableWidth和availableHeight (請記住這兩個值,下面還要用到),它們表示的是我們的ViewGroup總共可以獲得的屏幕區域大小(請仔細體會available的含義)。那么問題來了,假如我們的ViewGroup的parent是二球貨,給我們的ViewGroup設定的寬高小于我們的ViewGroup measured的寬高,讓我們的ViewGroup怎么優雅地layout自己的子View 呢?

答案是:我們的ViewGroup在layout自己的子View時,想怎么layout就怎么layout,可以diao,也可以不diao parent給自己設定的尺寸。

為什么是這樣呢?既然可以不diao這個尺寸,為什么我們的ViewGroup還要辛苦地在onMeasure方法中計算每一個子View的寬高,還二乎乎地將它們的尺寸加起來,告訴它的parent呢?我頭有點暈,讓我歇一會兒。好吧,看張美圖提提神!

圖片來源網絡-如侵刪

2、為了優雅地layout,必須先把尺寸的問題搞明白

上文中,ViewGroup在自己的layout方法中,獲得了parent給自己設定的尺寸大小,即 availableWidth和availableHeight ,這個值相當于parent告訴ViewGroup:“請以你的左上角為圓點,向右為x,向下為y的坐標系,給你的每一個子View確定位置和大小。我可以向你保證,這個坐標系中的點P1(0,0)、點P2(availableWidth,0)、點P3(0,availableHeight)、點P4(availableWidth,availableHeight)組成的方框區域內的子View都可以獲得在手機屏幕(這里指硬件意義上的屏幕)上展示自己的機會。這個方框之外的子View,能不能在手機屏幕上展示自己,我就管不了了。”從這里我們看到,parent給我們的ViewGroup設定的尺寸,并不一定就完全對應著手機屏幕上的一塊相同大小的區域,在有些情況下,parent給我們的ViewGroup設定的這個尺寸可能比整個手機屏幕還大。但是,parent仍然向我們保證,在該區域內layout的子View,都能獲得在手機屏幕上展示自己的機會,parent是如何做到這一點的呢?答案是:通過parent的scroll功能。這里我們不詳細敘述scroll,如果你不是很理解,請查看相關資料。

好奇的我們可能要問:“假如我是一個ViewGroup,我把一個子View的一部分layout在了parent給定的區域內,另一部分超出了該區域,這個子View是不是最多只能獲得部分展示自己的機會?”不用懷疑,答案是:Yes!

你可能還要問:“那些在完全被layout在parent限定的區域之外的子View應該怎么辦呢?它們難道就該在無邊黑暗中永不見天日嗎?”這確實有點殘酷,所以,作為一個ViewGroup,你可以有三個選擇:

選擇一:很簡單,不要將子View 放到這個區域之外,萬事大吉!

如果這個ViewGroup的子View數量太多,parent給限定的區域實在放不下它們怎么辦?此時ViewGroup可以讓子View重疊,以便所有的子View能夠在parent限定的區域內layout出來。像下面這樣:

dieluohan.jpg

選擇二:讓你的ViewGroup實現scroll功能,從而確保parent限定區域外的子View也能夠有機會展示自己。

選擇三:將你的ViewGroup的parent換成ScrollView。這樣你的ViewGroup就不用自己實現scroll功能了。但是ScrollView只能允許子View的高度超過自己,不允許子View的寬度超過自己。所以,作為ViewGroup,可以在不超過availableWidth的情況下,將子View layout 到任意的高度上。如下圖所示:

ViewGroup-demo1.png

看到沒?作為一個優秀的ViewGroup,當你layout自己的子View時,只要保證子View在availableWidth之內,即使超過了parent要求的高度也沒有關系,開發者還是愿意使用你的,因為他們可以為你指定ScrollView作為parent。這就是我們看到許多的ViewGroup在layout 子View時, 寧超高度,不超寬度 的原因。

關于ScrollView怎樣實現的scroll功能,講起來比較復雜,我們暫時放下不表。

至此,你應該明白,上文中我們提出的,對于parent指定的availableWidth和availableHeight,作為ViewGroup還是要盡量不超過parent限定的區域,如果一定要超過的話,那就超availableHeight,而不要超availableWidth。

3、讓我們了解一下layout_gravity

我們看到,Android系統提供的FrameLayout、LinearLayout等都支持子View設定layout_gravity,它到底是干什么用的?我們自己自定義ViewGroup時能不能也用上它?

關于它的作用,一句話就能說明白,當ViewGroup給子View分配的空間超過子View要求的大小時,就需要gravity幫助ViewGroup為子View精確定位。可見,layout_gravity就是ViewGroup在layout階段,協助ViewGroup給它的子View確定位置的,沒錯,就是協助確定子View的 left,top,bottom,right四個值。

下面,我們以FrameLayout為例來進行說明。假設FrameLayout中有一個子View,這個子View的所要求的展示尺寸(measuredWidth,measuredHeight)小于FrameLayout的尺寸,但是FrameLayout是個實心眼,它不管子View要求多大,都會把它所有的屏幕區域給子View,這樣就可以保證,用戶在這個區域中的交互動作,都是與子View的交互。那么問題來了,FrameLayout在layout子View時,總不能讓它的left和top為0,right和bottom等于自己的寬和高吧。如果這么干,子View就要在這個尺寸下,繪制自己,就不可避免地要對它包含的drawables進行拉伸,展示效果必然受到影響,那怎么辦?

FrameLayout會提取子View的 LayoutParams中的gravity,看看子View想在哪個位置,假設子View的layout_gravity的值是"top|left",那么FrameLayout就會把子View layout到自己的左上角,大小嘛就是子View所要求的大小。但是請注意,雖然此時子View繪制時是按照自己要求的大小繪制的,但是,能與它發生交互的區域卻是整個FrameLayout所占的屏幕區域。

所以,要不要使用layout_gravity,就看你自定義的ViewGroup是不是給子View分配大于它們要求的空間。

二、實戰自定義ViewGroup

好了,下面我們就通過實戰來檢驗下我們剛學到的自定義ViewGroup的知識。我們將定義的ViewGroup,名為StaggerLayout。它展示的效果是這樣的:

StaggerLayout-demo.png

代碼如下:

package com.milter.www.customviewgroupforblog;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by Administrator on 2016/8/14.
 */
public class StaggerLayout extends ViewGroup {
    public static final String TAG = "StaggerLayout" ;

    /*
    首先,定義好我們的四個構造方法,注意,ViewGroup的構造方法與上篇中的自定義View AnalogClock遵循相同的最佳實踐。
     */

    //第一個構造方法
    public StaggerLayout(Context context) {
        this(context, null);
    }
    //第二個構造方法
    public StaggerLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    //第三個構造方法
    public StaggerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);

    }
    //第四個構造方法
    public StaggerLayout(Context context, AttributeSet attrs,
                         int defStyleAttr, int defStyleRes) {

        super(context, attrs, defStyleAttr, defStyleRes);

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


        /*
        maxHeight和maxWidth就是我們最后計算匯總后的ViewGroup需要的寬和高。用來報告給ViewGroup的parent。

        在計算maxWidth時,我們首先簡單地把所有子View的寬度加起來,
        如果該ViewGroup所有的子View的寬度加起來都沒有
        超過parent的寬度限制,那么我們把該ViewGroup的measured寬度設為maxWidth,
        如果最后的結果超過了parent的寬度限制,我們就設置measured寬度為parent的限制寬度,
        這是通過對maxWidth進行resolveSizeAndState處理得到的。

        對于maxHeight,在每一行中找出最高的一個子View,然后把所有行中最高的子View加起來。
        這里我們在報告maxHeight時,也進行一次resolveSizeAndState處理。


         */
        int maxHeight = 0;
        int maxWidth = 0;

        /*
            mLeftHeight表示當前行已有子View中最高的那個的高度。當需要換行時,把它的值加到maxHeight上,
            然后將新行中第一個子View的高度設置給它。
            mLeftWidth表示當前行中所有子View已經占有的寬度,
當新加入一個子View導致該寬度超過parent的寬度限制時,
增加maxHeight的值,同時將新行中第一個子View的寬度設置給它。

         */

        int mLeftHeight = 0;
        int mLeftWidth = 0;

        final int count = getChildCount();
        Log.d(TAG,"Child count is " + count);
        final int widthSize =  MeasureSpec.getSize(widthMeasureSpec);

        Log.d(TAG,"widthSize in Measure is :"+ widthSize);


        // 遍歷我們的子View,并測量它們,根據它們要求的尺寸進而計算我們的StaggerLayout需要的尺寸。
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);

            //可見性為gone的子View,我們就當它不存在。
            if (child.getVisibility() == GONE)
                continue;

            // 測量該子View
            measureChild(child, widthMeasureSpec, heightMeasureSpec);

            //簡單地把所有子View的測量寬度相加。
            maxWidth += child.getMeasuredWidth();
            mLeftWidth += child.getMeasuredWidth();

            //這里判斷是否需將index 為i的子View放入下一行,如果需要,就要更新我們的maxHeight,mLeftHeight和mLeftWidth。
            if (mLeftWidth > widthSize) {
                maxHeight += mLeftHeight;
                mLeftWidth = child.getMeasuredWidth();
                mLeftHeight = child.getMeasuredHeight();

            } else {

                mLeftHeight = Math.max(mLeftHeight,   child.getMeasuredHeight());
            }

        }

        //這里把最后一行的高度加上,注意不要遺漏。
        maxHeight += mLeftHeight;

        //這里將寬度和高度與Google為我們設定的建議最低寬高對比,確保我們要求的尺寸不低于建議的最低寬高。
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        //報告我們最終計算出的寬高。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
                resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

            final int count = getChildCount();

            //childLeft和childTop代表在StaggerLayout的坐標系中,能夠用來layout子View的區域的
            //左上角的頂點的坐標。
            final int childLeft = getPaddingLeft();
            final int childTop = getPaddingTop();

            //childRight代表在StaggerLayout的坐標系中,能夠用來layout子View的區域的
            //右邊那條邊的坐標。
            final int childRight = r -  l - getPaddingRight();


        /*
          curLeft和curTop代表StaggerLayout準備用來layout子View的起點坐標,這個點的坐標隨著
          子View一個一個地被layout,在不斷變化,有點像數據庫中的Cursor,指向下一個可用區域。
          maxHeight代表當前行中最高的子View的高度,當需要換行時,curTop要加上該值,以確保新行中
          的子View不會與上一行中的子View發生重疊。
         */
           int curLeft, curTop, maxHeight;

            maxHeight = 0;
            curLeft = childLeft;
            curTop = childTop;

            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);

                if (child.getVisibility() == GONE)
                    return;

                int curWidth, curHeight;
                curWidth = child.getMeasuredWidth();
                curHeight = child.getMeasuredHeight();
                //用來判斷是否應當將該子View放到下一行
                if (curLeft + curWidth >= childRight) {
                    /*
                    需要移到下一行時,更新curLeft和curTop的值,使它們指向下一行的起點
                    同時將maxHeight清零。
                     */
                    curLeft = childLeft;
                    curTop += maxHeight;
                    maxHeight = 0;
                }
                //所有的努力只為了這一次layout
                child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);
                //更新maxHeight和curLeft
                if (maxHeight < curHeight)
                    maxHeight = curHeight;
                curLeft += curWidth;
            }
    }


}

好了,這樣我們就基本掌握了自定義ViewGroup了。實際上,自定義ViewGroup是一個可難可簡的事,關鍵是要滿足自己的需求。如果要定義出一個能夠滿足大多數開發者使用需求的自定義ViewGroup,就像LinearLayout和RelativeLayout那樣的,還是很有難度的,如果你不信,你可以看看它們的源碼。

 

來自:http://www.jianshu.com/p/5e61b6af4e4c

 

 本文由用戶 ybsoft 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!