Android自定義控件(十一)——自定義ViewGroup實現LinearLayout


最後實現效果

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下載地址:點擊下載

發佈了109 篇原創文章 · 獲贊 144 · 訪問量 105萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章