ANDROID自定義檢視——onLayout原始碼 流程 思路詳解
簡介:
在自定義view的時候,其實很簡單,只需要知道3步驟:
1.測量——onMeasure():決定View的大小
2.佈局——onLayout():決定View在ViewGroup中的位置
3.繪製——onDraw():如何繪製這個View。
而第3步的onDraw系統已經封裝的很好了,基本不用我們來操心,只需要專注到1,2兩個步驟就中好了。
第一步的測量,可以參考我之前的文章:(ANDROID自定義檢視——onMeasure流程,MeasureSpec詳解)
而這篇文章就來談談第二步:“佈局(Layout)”
知識點回顧:
在談如何使用onLayout方法前,先簡單回憶一下知識點:
View檢視結構:
View檢視可以是單一的一個如TextView,也可以是一個檢視組(ViewGroup)如LinearLayout。
這個樹的概念很重要,因為無論我們是在測量大小或是調整佈局的時候都是從樹的頂端開始一層一層,一個分支一個分支的進行(樹形遞迴)。
Measure簡單回顧:
measure的作用就是為整個View樹計算實際的大小,而通過剛才對View樹的介紹知道,想計算整個View樹的大小,就需要遞迴的去計算每一個子檢視的大小(Layout同理)。
對每一個檢視通過onMeasure方法的一系列測量流程後計算出實際的高(mMeasuredHeight)和寬(mMeasureWidth)傳入setMeasuredDimension()方法完成單個View的測量,如果所測的檢視是ViewGroup則可以通過measureChild方法遞迴的計算其中的每一個子view。對於每個View的實際寬高都是由父檢視和本身檢視決定的。
Layout(原始碼分析):
Layout的作用就是為整個View樹計算實際的位置,而通過剛才對View樹的介紹知道,想計算整個View樹的位置,就需要遞迴的去計算每一個子檢視的位置(Measure同理)。
而確定這個位置很簡單,只需要mLeft,mTop,mRight,mBottom四個值(注意:這4個值是子View相對於父View的值,下面會詳細介紹)。
在程式碼中如何設定這4個值呢?
首先,無論是系統提供的LinearLayout還是我們自定義的View檢視,他都需要繼承自ViewGroup類,之後必須要做的就是重寫onLayout方法(因為在onLayout在ViewGroup中被定義為抽象方法)。
ViewGroup-onlayout:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
onLayout被定義為抽象方法,所以在繼承ViewGroup時必須要重寫該方法(onMeasure不需要)。另外這個方法也被override標註,所以也是重寫的方法,他重寫的是其父類view中的onLayout方法。
View-onlayout:
/**
* 當這個view和其子view被分配一個大小和位置時,被layout呼叫。
* @param changed 當前View的大小和位置改變了
* @param left 左部位置(相對於父檢視)
* @param top 頂部位置(相對於父檢視)
* @param right 右部位置(相對於父檢視)
* @param bottom 底部位置(相對於父檢視)
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
註解說:當這個view和其子view被分配一個大小和位置時,被layout呼叫。所以我們去看看layout中做了什麼。(註解沒有完全按照英文翻譯,並且有省略)
View-layout:
/**
* 給View和其所有子View分配大小和位置
*
* 這是佈局的第二個階段(第一個階段是測量)。在這個階段中,每個父檢視需要去呼叫layout去為他所有的子檢視確定位置
* 派生的子類不應該重寫layout方法,應該重寫onLayout方法,在onlayout方法中應該去呼叫每一個view的layout
*/
public void 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去實現。
計算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左邊界的距離
public final int getLeft() {
return mLeft;
}
view.getTop()——mTop:子View上邊界到父view上邊界的距離
view.getRight()——mRight:子View右邊界到父view左邊界的距離
view.getBottom()——mBottom:子View下邊距到父View上邊界的距離
檢視寬高:
檢視寬度 view.getWidth();子View的右邊界 - 子view的左邊界。
public final int getWidth() {
return mRight - mLeft;
}
檢視高度 view.getHeight() ;子View的下邊界 - 子view的上邊界。
public final int getHeight() {
return mBottom - mTop;
}
測量寬高:
view.getMeasuredWidth();measure過程中返回的mMeasuredWidth
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
view.getMeasuredHeight();measure過程中返回的mMeasuredHeight
public final int 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(因為上下間距一樣)
我的程式碼如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 迴圈所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 取出當前子View長寬
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
// 計算當前的mLeft和mTop值(r,b為傳遞進來的父View的mRight和mBottom值)
int mLeft = (r - width) / 2;
int mTop = (b - height) / 2;
// 呼叫layout並傳遞計算過的引數為子view佈局
child.layout(mLeft, mTop, mLeft + width, mTop + height);
}
佈局檔案如下:
<com.gxy.text.CostomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#eee999" >
<Button
android:text="ChildView"
android:layout_width="200dip"
android:layout_height="200dip"
android:background="#333444"
android:id="@+id/textView2" />
</com.gxy.text.CostomViewGroup>
總結:
onMeasure和onLayout的大致總結完了,在自定義View的時候最關鍵的是onLayout,因為無論你如何Measure這個View的大小,最後的決定權永遠在onLayout手中,onLayout會決定具體View的大小和位置。當然onMeasure也很重要,有的情況控制元件的寬高不確定或者需要自定義,這時候需要我們人工Measure它。而在複雜的自定義View時,很多計算也需要在onMeasure中完成,並且些值會記錄下來在onLayout中重新使用(個人理解,歡迎指正)。
寫onMeasure和onLayout的時候只是想自己總結一下,整理一下思路。因為網上有太多寫的好了,這裡推薦一下qinjuning這位大神的blog,關於View的內容他總結的相當全面和深入。
雖然有很好的了,但我還會堅持自己總結一遍,接下來的計劃是寫一個稍微複雜點的小例子,將onMeasure和onLayout結合起來(已經寫完了,連結如下:ANDROID自定義檢視——仿瀑布佈局)。之後深入的研究一下View和ViewGroup的原始碼,總結一下LayoutParams,LayoutInflater等簡單常用的知識點,瞭解一下View的繪製重新整理流程等等等。。。太多不會的了,慢慢來吧。