Android自定義控制元件之onLayout
一、簡介
在自定義view的時候,其實很簡單,只需要知道3步驟:
1.測量——onMeasure():決定View的大小,關於此請閱讀《Android自定義控制元件之onMeasure》
2.佈局——onLayout():決定View在ViewGroup中的位置
3.繪製——onDraw():如何繪製這個View。
這篇文章主要來談談第二步佈局(Layout)
二、View檢視結構
View檢視可以是單一的一個如TextView,也可以是一個檢視組(ViewGroup)如LinearLayout。
如圖:對於多View的檢視他的結構是樹形結構,最頂層是ViewGroup,ViewGroup下可能有多個ViewGroup或View。
這個樹的概念很重要,因為無論我們是在測量大小或是調整佈局的時候都是從樹的頂端開始一層一層,一個分支一個分支的進行(樹形遞迴)。
三、onLayout函式
measure的作用就是為整個View樹計算實際的大小,而通過剛才對View樹的介紹知道,想計算整個View樹的大小,就需要遞迴的去計算每一個子檢視的大小(Layout同理)。
對每一個檢視通過onMeasure方法的一系列測量流程後計算出實際的高(mMeasuredHeight)和寬(mMeasureWidth)傳入setMeasuredDimension()方法完成單個View的測量,如果所測的檢視是ViewGroup則可以通過measureChild
Layout的作用就是為整個View樹計算實際的位置,而通過剛才對View樹的介紹知道,想計算整個View樹的位置,就需要遞迴的去計算每一個子檢視的位置(Measure同理)。
而確定這個位置很簡單,只需要mLeft,mTop,mRight,mBottom四個值(注意:這4個值是子View相對於父View的值,下面會詳細介紹)。
在程式碼中如何設定這4個值呢?
首先,無論是系統提供的LinearLayout還是我們自定義的View檢視,他都需要繼承自ViewGroup類,之後必須要做的就是重寫onLayout方法(因為在onLayout在ViewGroup中被定義為抽象方法)。
ViewGroup的onlayout:
[java] view plain copy print?- @Override
- protectedabstractvoid onLayout(boolean changed, int l, int t, int r, int b);
View.java的onLayout:
[java] view plain copy print?- /**
- * 當這個view和其子view被分配一個大小和位置時,被layout呼叫。
- * @param changed 當前View的大小和位置改變了
- * @param left 左部位置(相對於父檢視)
- * @param top 頂部位置(相對於父檢視)
- * @param right 右部位置(相對於父檢視)
- * @param bottom 底部位置(相對於父檢視)
- */
- protectedvoid onLayout(boolean changed, int left, int top, int right, int bottom) {}
View的layout:
- /**
- * 給View和其所有子View分配大小和位置
- *
- * 這是佈局的第二個階段(第一個階段是測量)。在這個階段中,每個父檢視需要去呼叫layout去為他所有的子檢視確定位置
- * 派生的子類不應該重寫layout方法,應該重寫onLayout方法,在onlayout方法中應該去呼叫每一個view的layout
- */
- publicvoid layout(int l, int t, int r, int b) {
- // 將當前檢視的左上右下記錄為old值(引數中傳入的為新的l,t,r,b值)
- int oldL = mLeft;
- int oldT = mTop;
- int oldB = mBottom;
- int oldR = mRight;
- // setFrame方法的作用就是將新傳入的ltrb屬性賦值給View,然後判斷當前View大小和位置是否發生了變化並返回
- boolean changed = setFrame(l, t, r, b);
- if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
- // 呼叫onLayout回撥方法,具體實現由重寫了onLayout方法的ViewGroup的子類去實現(後面詳細說明)
- onLayout(changed, l, t, r, b);
- mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
- // 呼叫所有重寫了onLayoutChange監聽的方法,通知View大小和位置發生了改變
- ListenerInfo li = mListenerInfo;
- if (li != null && li.mOnLayoutChangeListeners != null) {
- ArrayList<OnLayoutChangeListener> listenersCopy =
- (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
- int numListeners = listenersCopy.size();
- for (int i = 0; i < numListeners; ++i) {
- listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
- }
- }
- }
- mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
- }
在這段程式碼中我們只要知道:如果檢視的大小和位置發生變化後,會呼叫我們前面分析過的onLayout方法。
對於onLayout方法的最終實現全部依靠我們在自定義ViewGroup類中重寫的onLayout去實現。
ViewGroup.java的layout函式
/**
* {@inheritDoc}
*/
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
四、計算View位置
重寫的onLayout方法中,唯一的目的就是:對當前檢視和其所有子View設定它們在父檢視中具體位置(確定這個位置就依靠mLeft,mTop,mRight,mBottom這四個值)
之前介紹過,mLeft,mTop,mRight,mBottom這四個值表示的是子view相對於父view的位置。下面我貼出我畫的圖看一下就明白了。
如圖,黃色區域是我們的父view,而中間的深色的區域就是我們的子view。
所以對於這個View來說,我列出它相對於父view的各個值是如何計算和相關函式:
mLeft,mTop,mRight,mBottom:
view.getLeft()——mLeft:子View左邊界到父view左邊界的距離
[java] view plain copy print?- publicfinalint getLeft() {
- return mLeft;
- }
view.getTop()——mTop:子View上邊界到父view上邊界的距離
view.getRight()——mRight:子View右邊界到父view左邊界的距離
view.getBottom()——mBottom:子View下邊距到父View上邊界的距離
檢視寬高:
檢視寬度 view.getWidth();子View的右邊界 - 子view的左邊界。
[java] view plain copy print?- publicfinalint getWidth() {
- return mRight - mLeft;
- }
[java] view plain copy print?
- publicfinalint getHeight() {
- return mBottom - mTop;
- }
測量寬高:
view.getMeasuredWidth();measure過程中返回的mMeasuredWidth
[java] view plain copy print?- publicfinalint getMeasuredWidth() {
- return mMeasuredWidth & MEASURED_SIZE_MASK;
- }
[java] view plain copy print?
- publicfinalint getMeasuredHeight() {
- return mMeasuredHeight & MEASURED_SIZE_MASK;
- }
最後介紹一下getWidth/Height和getMeasuredWidth/Height的區別:
getWidth,和getLeft等這些函式都是View相對於其父View的位置。而getMeasuredWidth,getMeasuredHeight是測量後該View的實際值(有點繞,下面摘錄一段jafsldkfj所寫的Blog中的解釋).
實際上在當螢幕可以包裹內容的時候,他們的值是相等的,只有當view超出屏幕後,才能看出他們的區別:
getMeasuredHeight()是實際View的大小,與螢幕無關,而getHeight的大小此時則是螢幕的大小。
當超出屏幕後,getMeasuredHeight()等於getHeight()加上螢幕之外沒有顯示的大小
在計運算元View在父View中的位置時,主要就是應用上面這幾個函式。下面就來看看如何去重寫onLayout。
五、重寫onLayout
對於重寫onLayout的思路和重寫onMeasure相同:
如果只需要測量單個View,則單獨測量它自己就行。如果需要測量的View其下還有子View,則需要測量其所有的子View。
就以上面的View為例子,他最外面是一個黃色的父View,中間一個居中的深色子View。 我的思路如下: 如果想畫出一個View,就要計算它的l,t,r,b值。並傳遞到onlayout( l, t, r, b )中; mRight = view.getWidth + mLeft; mBottom = view.getHeight + mTop; 所以最後可以用如下形式傳入:onlayout( l, t, l+width, t+height );剩下的任務就只需要知道它的mLeft值,mTop值,加上長、寬值就行了。
長寬值很簡單,使用getWidth/Height和getMeasuredWidth/Height都可以。
由於這個View需要居中顯示,剩下的問題就是如何計算該View的mLeft值和mTop值。我的思路如下:
r(父View的mRight) = mLeft + width + mLeft(因為左右間距一樣)
b(父View的mBottom) = mTop + height + mTop(因為上下間距一樣)
我的程式碼如下:
[java] view plain copy print?- @Override
- protectedvoid onLayout(boolean changed, int l, int t, int r, int b) {
- // 迴圈所有子View
- for (int