在自定義view的時候,其實很簡單,只須要知道3步驟:
1.測量——onMeasure():決定View的大小
2.佈局——onLayout():決定View在ViewGroup中的位置
3.繪製——onDraw():如何繪製這個View。java
而第3步的onDraw系統已經封裝的很好了,基本不用咱們來操心,只須要專一到1,2兩個步驟就中好了。
第一步的測量,能夠參考我以前的文章:(ANDROID自定義視圖——onMeasure流程,MeasureSpec詳解)
而這篇文章就來談談第二步:「佈局(Layout)」android
知識點回顧:
在談如何使用onLayout方法前,先簡單回憶一下知識點:web
View視圖結構:
View視圖能夠是單一的一個如TextView,也能夠是一個視圖組(ViewGroup)如LinearLayout。
如圖:對於多View的視圖他的結構是樹形結構,最頂層是ViewGroup,ViewGroup下可能有多個ViewGroup或View。ide
這個樹的概念很重要,由於不管咱們是在測量大小或是調整佈局的時候都是從樹的頂端開始一層一層,一個分支一個分支的進行(樹形遞歸)。函數
measure的做用就是爲整個View樹計算實際的大小,而經過剛纔對View樹的介紹知道,想計算整個View樹的大小,就須要遞歸的去計算每個子視圖的大小(Layout同理)。
對每個視圖經過onMeasure方法的一系列測量流程後計算出實際的高(mMeasuredHeight)和寬(mMeasureWidth)傳入setMeasuredDimension() 方法完成單個View的測量,若是所測的視圖是ViewGroup則能夠經過measureChild方法遞歸的計算其中的每個子view。對於每一個View的實際寬高都是由父視圖和自己視圖決定的。源碼分析
Layout的做用就是爲整個View樹計算實際的位置,而經過剛纔對View樹的介紹知道,想計算整個View樹的位置,就須要遞歸的去計算每個子視圖的位置(Measure同理)。佈局
而肯定這個位置很簡單,只須要mLeft,mTop,mRight,mBottom四個值(注意:這4個值是子View相對於父View的值,下面會詳細介紹)。this
在代碼中如何設置這4個值呢?
首先,不管是系統提供的LinearLayout仍是咱們自定義的View視圖,他都須要繼承自ViewGroup類,以後必需要作的就是重寫onLayout方法(由於在onLayout在ViewGroup中被定義爲抽象方法)。spa
ViewGroup-onlayout:.net
@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去實現。
在重寫的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的思路和重寫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的內容他總結的至關全面和深刻。