在學習一個技術點的時候,我會先搞清楚它的基本原理,而後再動手編碼,由於我但願對本身寫的每一行代碼最終將會怎樣執行有精準的把握。不然,我寫代碼時就沒有底氣,就像在棉花堆上走路,每一步都會內心發虛。學習自定義ViewGroup固然也不例外。下面,咱們就一塊兒看看自定義ViewGroup的原理吧。java
經過上篇文章,咱們知道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順利完成本身的交互任務。數據庫
對於上面的圖,有兩點很是容易讓人產生誤解,須要解釋一下:網絡
//注意,這個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左上角在屏幕上的位置,是由系統幫咱們決定的,咱們不用操那麼多心。
//注意,這個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
圖片來源網絡-如侵刪學習
上文中,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。
咱們看到,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);
好了,下面咱們就經過實戰來檢驗下咱們剛學到的自定義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 著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。