ViewGroup的繪製流程
要自定以ViewGroup,我們首先需要了解ViewGroup的繪製流程,其實View與ViewGroup繪製基本相同,只是在ViewGroup中,不僅僅要繪製自己,還要繪製其中的子控件,所以ViewGroup的繪製流程分爲三步:測量,佈局,繪製,分別對應onMeasure(),onLayout(),onDraw()。
1.onMeasure():測量當前控件的大小,爲正式佈局提供建議,注意僅僅只是建議,至於用不用看onLayout()。
2.onLayout():使用layout()函數對所有子控件進行佈局。
3.onDraw():根據佈局的位置繪圖。
這裏onDraw()就不再贅述了,前面自定義的所有View()基本都講解過如何使用onDraw(),本博文重點介紹onMeasure()和onLayout()函數。
onMeasure()函數與MeasureSpec
首先,我們來看看onMeasure() 函數的定義:
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec)
這裏主要傳進去兩個參數,分別是widthMeasureSpec和heightMeasureSpec。它們是父類傳遞過來給當前ViewGroup的一個建議值,即想把當前ViewGroup的尺寸設置爲寬widthMeasureSpec,高heightMeasureSpec。
雖然他們兩個是int類型,但其實他們是由mode+size兩部分組成的,轉換位二進制都是32位的,前2位代表模式mode,後30位代表數值。
模式分類
既然說到mode模式,我們來看看它的三種分類:
(1)UNSPECIFIED(未指定):父元素不對子元素施加任何束縛,子元素可以得到任何想要的數值。
(2)EXACTLY(完全):父元素決定子元素的確切大小,子元素將被限定在給定的邊界裏而忽略它本身的大小。
(3)AT_MOST(至多):子元素至多達到指定大小的值。
既然提到了模式,很顯然,我們提取模式進行判斷,就需要聽過與運算得到,這裏Android給我們提供了一個簡單的方法,直接提取:
MeasureSpec.getMode(int spec)//獲取模式
MeasureSpec.getSize(int spec)//獲取數值
那麼代碼中使用起來的代碼就是這樣:
int measureWidth=MeasureSpec.getSize(widthMeasureSpec)
int measureWidhtMode=MeasureSpec.getMode(widthMeasureSpec)
//heightMeasureSpec同樣如此使用。
如何使用模式
我們先來看看一般我們在XML中如何定義控件的寬高的,有如下三種方式:
(1)warp_content:對應模式MeasureSpec.AT_MOST。
(2)match_parent:對應模式的MeasureSpec.EXACTLY。
(2)具體值(比如設置60px,60dp等):對應模式的MeasureSpec.EXACTLY。
我們從這裏看出來,一般我們用不到MeasureSpec.UNSPECIFIED模式。但是我們這裏需要注意的是,如果我們設置爲MeasureSpec.EXACTLY模式,就不必設置我們計算的大小數值,因爲用戶已經指定,而設置爲MeasureSpec.AT_MOST(warp_content)就需要我們設置具體數值。所以,我們自定義ViewGroup的onMeasure()函數一般都是這樣的:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth=MeasureSpec.getSize(widthMeasureSpec);
int measureHeight=MeasureSpec.getSize(heightMeasureSpec);
int measureWidhtMode=MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode=MeasureSpec.getMode(heightMeasureSpec);
//這裏計算width,height
setMeasuredDimension((measureWidhtMode==MeasureSpec.EXACTLY)?measureWidth:width,(measureHeightMode==MeasureSpec.EXACTLY)?measureHeight:height);
}
如果等於MeasureSpec.EXACTLY就不需要進行計算設置,如果是MeasureSpec.AT_MOST(warp_content)就需要計算控件大小的步驟。
onLayout()函數
前面已經說過了,onLayout()函數是實現所有子控件佈局的函數,需要注意的是,這裏是實現所有子控件的佈局,至於自己我們後面會介紹。我們先來看看onLayout()函數的定義:
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到這是一個抽象函數,說明只要你需要自定義ViewGroup,就必須實現該函數,想我們後面自定義ViewGroup實現LinearLayout一樣,都需要重寫這個函數,然後按照自己的規則,對子控件進行佈局。
自定義ViewGroup實現LinearLayout
我們假設,我們需要自定義的ViewGroup是一個垂直佈局,所以我們知道,整體控件的高就是子控件高的和,寬度就是子控件中最寬的哪個,所以我們的onMeasure()函數實現如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth=MeasureSpec.getSize(widthMeasureSpec);
int measureHeight=MeasureSpec.getSize(heightMeasureSpec);
int measureWidhtMode=MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode=MeasureSpec.getMode(heightMeasureSpec);
//這裏計算width,height
int height=0;
int width=0;
int count=getChildCount();
for(int i=0;i<count;i++){
//測量子控件
View child=getChildAt(i);
measureChild(child,widthMeasureSpec,heightMeasureSpec);
int childWidth=child.getMeasuredWidth();
int childHeight=child.getMeasuredHeight();
height+=childHeight;//高度疊加
width=Math.max(width,childWidth);//寬度取最大
}
setMeasuredDimension((measureWidhtMode==MeasureSpec.EXACTLY)?measureWidth:width,(measureHeightMode==MeasureSpec.EXACTLY)?measureHeight:height);
}
可以看到,我們實現的原理,基本與上面講解的一致,獲取子控件最寬的寬度設置爲整體ViewGroup的寬度,設置ViewGroup的高度爲子控件高度和。因爲我們的高度寬度在XML中都設置爲了warp_content,這裏就需要我們自己計算。(XML代碼最後)
接着,就是實現我們的onLayout()函數,因爲我們說了我們是實現垂直佈局的LinearLayout,所以我們需要在這個函數中佈局子控件的位置。代碼如下:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top=0;
int count=getChildCount();
for(int i=0;i<count;i++){
View child=getChildAt(i);
int childWidth=child.getMeasuredWidth();
int childHeight=child.getMeasuredHeight();
child.layout(0,top,childWidth,childHeight);
top+=childHeight;
}
}
因爲我們是垂直佈局,也沒有設置什麼pading,margin,所以左上角座標就只有top在變化疊加,也就是加上第一個子控件的高度就是第二個子控件的top。我們的XML代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<com.liyuanjinglyj.customviewgroup.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_red_dark"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是第一個子控件"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是第二個子控件"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是第三個子控件(但我加長了)"/>
</com.liyuanjinglyj.customviewgroup.MyViewGroup>
但是我們還是需要注意一下getMeasuredWidth()與getWidth()區別,getMeasuredWidth()函數在onMeasure()過程結束後,就可以獲取到寬度值,而getWidth()函數要在onLayout()過程結束後才能獲取到寬度值,所以我們上面都使用getMeasuredWidth()。
而且getMeasuredWidth()函數是通過setMeasuredDimension()函數進行設置的,getWidth()函數則是通過layout()函數來設置的。所以我們在前面自定義的所有View中都是在onDraw()中使用getWidth(),因爲其他地方必須等onMeasure()與onLayout()指定完後才能獲取到。
本文Github下載地址:點擊下載