自定義ViewGroup本質是什麼?
自定義ViewGroup本質上就幹一件事——layout
。
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)
複製程式碼
如圖所示:
這個方法,既確定了子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呢?
ViewGroup如何優雅的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出來。 - 選擇二:
讓你的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
。
瞭解一下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現在要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;
}
}
}
複製程式碼
目錄結構
- Android 自定義View基礎(一)
- Android自定義View:View(二)
- Android 自定義View:處理事件分發(四)
- Android 自定義View:屬性動畫(六)
- Android 自定義View:深入理解自定義屬性(七)