教你步步爲營掌握自定義ViewGroup

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

在學習一個技術點的時候,我會先搞清楚它的基本原理,而後再動手編碼,由於我但願對本身寫的每一行代碼最終將會怎樣執行有精準的把握。不然,我寫代碼時就沒有底氣,就像在棉花堆上走路,每一步都會內心發虛。學習自定義ViewGroup固然也不例外。下面,咱們就一塊兒看看自定義ViewGroup的原理吧。java

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

經過上篇文章,咱們知道ViewGroup是一個組合View,它與普通的基本View(只要不是ViewGroup,都是基本View)最大的區別在於,它能夠容納其餘View,這些View既能夠是基本View,也能夠ViewGroup,可是在咱們的ViewGroup眼中,無論是View仍是ViewGroup,它們都抽象成了一個普通的View,ViewGroup的最最根本的職責就是,在本身內部,給它們每個人找一個合適的位置,也就是調用它們的以下方法:android

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

以下圖所示:git

ViewGroup-demo.pnggithub

這個方法,可謂是一舉兩得,既肯定了子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給本身設定的尺寸。app

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

圖片來源網絡-如侵刪學習

二、爲了優雅地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,若是你不是很理解,請查看相關資料。this

好奇的咱們可能要問:「假如我是一個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。

三、讓咱們瞭解一下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分配大於它們要求的空間。

2016.08.15增長內容
----------------------------------------------------------------------------------------------------
有的朋友很好奇,私信問我,到底layout_gravity怎麼使用?
咱們就簡單說說。在Android中,layout_gravity可以取的值有left、bottom、center_horizontal等,這些值不是隨隨便便取的,也不是你想怎麼取就怎麼取,實際上,這些可取的值是一個有限的集合,定義在一個叫Gravity的類裏面。以下所示:

//該代碼段摘自Gravity.java類

    public static final int TOP = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;

    public static final int BOTTOM = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;

    public static final int LEFT = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_X_SHIFT;

    public static final int RIGHT = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_X_SHIFT;

    public static final int CENTER_VERTICAL = AXIS_SPECIFIED<<AXIS_Y_SHIFT;

    public static final int FILL_VERTICAL = TOP|BOTTOM;

    public static final int CENTER_HORIZONTAL = AXIS_SPECIFIED<<AXIS_X_SHIFT;

    public static final int FILL_HORIZONTAL = LEFT|RIGHT;

    public static final int CENTER = CENTER_VERTICAL|CENTER_HORIZONTAL;

    public static final int FILL = FILL_VERTICAL|FILL_HORIZONTAL;

在ViewGroup的layout階段,也是Gravity類幫助咱們處理子View的layout_gravity請求的。

下面我就舉一個簡單的例子來講明。

假設ViewGroup如今要layout一個子View,以下是該子View要求的尺寸大小:
final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight();

如今,ViewGroup要給這個子View設定位置和大小了。設定的位置和大小用以下四個參數表示:
bigLeft,bigTop,bigRight,bigBottom。
這四個值在ViewGroup的以左上角爲原點,向右x,向下y的座標系中構成了一個矩形。以下:
Rect bigRect = new Rect( bigLeft, bigTop, bigRight, bigBottom);
進一步假設這個bigRect的寬高大於子View要求的寬高(是爲了更明顯地說明layout_gravity的做用,實際狀況可能不是這樣的),以下圖所示:

gravity_demo.png

如今ViewGroup準備把bigRect區域所有分給子View,可是ViewGroup顯然不能直接這樣layout 子View:
child.layout(bigLeft,bigTop,bigRight,bigBottom);
這樣的話,child就要在bigRect區域內繪製本身,不可避免地要拉伸本身,致使展現的效果變差(想像一下10*10的圖片擴成100*100是什麼效果)。因此,咱們須要在bigRect內進一步爲子View定位,怎麼定位?

第一步就是讀出子View的LayoutParams對象中的layout_gravity值。以下:
final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int child_layout_gravity = lp.gravity;
從上面代碼能夠看出,layout_gravity最終是以整數的形式存放於子View的LayoutParams中的。

第二步就是構建一個空的Rect,準備接收爲子View定位後的四個座標值,以下:
Rect smallRect = new Rect();

第三步就是見證奇蹟的時刻,以下:
Gravity.apply(child_layout_gravity, childWidth, childHeight, bigRect, smallRect);
通過上面的調用,Gravity會在smallRect中存入依據子View的layout_gravity以及子View要求的尺寸,在bigRect中爲子View精肯定位後的座標值,注意這個座標值所在的座標系仍是ViewGroup的座標系。因此,咱們如今能夠愉快地layout子View了。

child.layout(smallRect.left, smallRect.top, smallRect.right, smallRect.bottom);

2、實戰自定義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那樣的,仍是頗有難度的,若是你不信,你能夠看看它們的源碼。本項目的源代碼已上傳到GitHub,請猛戳:https://github.com/like4hub/CustomViewGroupForBlog


 

文/milter(簡書做者) 原文連接:http://www.jianshu.com/p/5e61b6af4e4c 著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。

相關文章
相關標籤/搜索