自定義ViewGroup本質上就幹一件事——layout
。bash
咱們知道ViewGroup是一個組合View,它與普通的基本View(只要不是ViewGroup,都是基本View)最大的區別在於,它能夠容納其餘View,這些View既能夠是基本View,也能夠ViewGroup,可是在咱們的ViewGroup眼中,不論是View仍是ViewGroup,它們都抽象成了一個普通的View,ViewGroup的最最根本的職責就是,在本身內部,給它們每個人找一個合適的位置
,也就是調用它們的以下方法:app
public void layout(int left, int top, int right, int bottom)
複製代碼
如圖所示:ide
這個方法,既肯定了子View的位置,也肯定了子View的大小
,請注意,這個大小是由咱們的ViewGroup最後決定的分給該子View的屏幕區域大小
。post
通常狀況下,ViewGroup在設定這個大小時,會考慮子View的自身要求的,也就是它們measured的大小(getMeasuredWidth , getMeasuredHeight),一般最後給每一個子View設定的大小就是它們所要求的大小,但這不是絕對的。動畫
假若有一個二愣子性格的ViewGroup,它宣稱:「我全部的子View的大小都必須是30*30的尺寸!」,這種SB的ViewGroup在調用每一個子View的layout方法時,經過讓bottom-top=right-left=30,就把全部的子View最後佔據的屏幕區域設定爲30*30了,無論各個子View所要求的大小是多少,此時都沒有任何用處了。ui
固然,除了有特殊需求,我相信沒人願意用這種ViewGroup的,這裏咱們能夠知道,咱們自定義ViewGroup,大致上有兩條路可選:this
**那麼使用者使用一個ViewGroup最基本的指望是什麼?**我想,應該是使用者放入這個ViewGroup中的子View,layout出來的尺寸和每一個子View measured的尺寸相符。只有這樣,才能確保使用者的每一個子View順利完成本身的交互任務。spa
對於上面的圖,有兩點很是容易讓人產生誤解,須要解釋一下:code
關於left、right、top、bottom。它們都是座標值,既然是座標值,就要明確座標系,這個座標系是什麼?咱們知道,這些值都是ViewGroup設定的,那麼,這個座標系天然也是由ViewGroup決定的了。這個座標系就是以ViewGroup左上角爲原點,向右x,向下y構建起來的。
cdn
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呢?
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出來。讓你的ViewGroup實現scroll功能
,從而確保parent限定區域外的子View也可以有機會展現本身。將你的ViewGroup的parent換成ScrollView
。這樣你的ViewGroup就不用本身實現scroll功能了。可是ScrollView只能容許子View的高度超過本身,不容許子View的寬度超過本身。因此,做爲ViewGroup,能夠在不超過availableWidth的狀況下,將子View layout 到任意的高度上。以下圖所示:看到沒?做爲一個優秀的ViewGroup,當你layout本身的子View時,只要保證子View在availableWidth以內,即便超過了parent要求的高度也沒有關係,開發者仍是願意使用你的,由於他們能夠爲你指定ScrollView做爲parent。
這就是咱們看到許多的ViewGroup在layout子View時,寧超高度,不超寬度的緣由。
至此,你應該明白,上文中咱們提出的,對於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分配大於它們要求的空間。
下面我就舉一個簡單的例子來講明。
假設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的做用,實際狀況可能不是這樣的),以下圖所示:
如今ViewGroup準備把bigRect區域所有分給子View,可是ViewGroup顯然不能直接這樣layout 子View:
child.layout(bigLeft,bigTop,bigRight,bigBottom);
複製代碼
這樣的話,child就要在bigRect區域內繪製本身,不可避免地要拉伸本身,致使展現的效果變差(想像一下1010的圖片擴成100100是什麼效果)。因此,咱們須要在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,名爲CustomLayout,效果以下:
代碼以下,註釋的很清晰:
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context) {
this(context, null);
}
public CustomLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
@TargetApi(21)
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, 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();
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 遍歷咱們的子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,在不斷變化。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) {
continue;
}
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;
}
}
}
複製代碼