1. 程式人生 > >Android自定義控制元件系列七:詳解onMeasure()方法中如何測量一個控制元件尺寸(一)

Android自定義控制元件系列七:詳解onMeasure()方法中如何測量一個控制元件尺寸(一)

自定義view/viewgroup要重寫的幾個方法:onMeasure(),onLayout(),onDraw()。(不熟悉的話可以檢視專欄的前幾篇文章:)。

        今天的任務就是詳細研究一下protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。

        如果只是說要重寫什麼方法有什麼用的話,還是不太清楚。先去原始碼中看看為什麼要重寫onMeasure()方法,這個方法是在哪裡呼叫的:

一、原始碼中的measure/onMeasure方法:


  1. protectedvoid onMeasure(
    int widthMeasureSpec, int heightMeasureSpec) {  
  2.     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
  3.             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
  4. }  

        實際上是在View這個類中的public final void measure(int widthMeasureSpec, int heightMeasureSpec)方法中被呼叫的:

  1. publicfinalvoid measure(int widthMeasureSpec, int heightMeasureSpec) {  
  2. ...  
  3. onMeasure(widthMeasureSpec, heightMeasureSpec);  
  4. ...  
  5. }  

1、measure()

可以看到,measure()這個方法是一個由final來修飾的方法,意味著不能夠被子類重寫.measure()方法的作用是:測量出一個View的實際大小,而實際性的測量工作,Android系統卻並沒有幫我們完成,因為這個工作交給了onMeasure()來作,所以我們需要在自定義View的時候按照自己的需求,重寫onMeasure方法.而子控制元件又分為view和viewGroup兩種情況,那麼測量的流程是怎樣的呢,看一下下面這個圖你就明白了:



2、onMeasure

onMeasure(int widthMeasureSpec, int heightMeasureSpec)中,兩個引數的作用:        widthMeasureSpec和heightMeasureSpec這兩個int型別的引數,看名字應該知道是跟寬和高有關係,但它們其實不是寬和高,而是由寬、高和各自方向上對應的模式來合成的一個值:其中,在int型別的32位二進位制位中,31-30這兩位表示模式,0~29這三十位表示寬和高的實際值.其中模式一共有三種,被定義在Android中的View類的一個內部類中:View.MeasureSpec:

UNSPECIFIED:表示預設值,父控制元件沒有給子view任何限制。------二進位制表示:00

②EXACTLY:表示父控制元件給子view一個具體的值,子view要設定成這些值的大小。------二進位制表示:01

③AT_MOST:表示父控制元件個子view一個最大的特定值,而子view不能超過這個值的大小。------二進位制表示:10

二、MeasureSpec

MeasureSpe描述了父View對子View大小的期望.裡面包含了測量模式和大小.我們可以通過以下方式從MeasureSpec中提取模式和大小,該方法內部是採用位移計算.

int specMode = MeasureSpec.getMode(measureSpec);//得到模式

int specSize = MeasureSpec.getSize(measureSpec);//得到大小

也可以通過MeasureSpec的靜態方法把大小和模式合成,該方法內部只是簡單的相加.

MeasureSpec.makeMeasureSpec(specSize,specMode);

每個View都包含一個ViewGroup.LayoutParams類或者其派生類,LayoutParams中包含了View和它的父View之間的關係,而View大小正是View和它的父View共同決定的。

我們平常使用類似於RelativeLayout和LinearLayout的時候,在其內部新增view的時候,不管是佈局檔案中加入還是在程式碼中使用addView方法新增,實際上都會呼叫這個onMeasure方法,而measure和onMeasure中的兩個引數,是由各級父控制元件往子控制元件/子view進行一層層傳遞的。我們可以在xml中定義Layout的寬和高的具體的值或寬高的填充方式:matchparent/wrapcontent,也可以在程式碼中使用LayoutParams設定,而實際上這裡設定的值就會對應到上面的measure和onMeasure方法中的兩個引數的模式,對應關係如下:

具體的值(如width=200dp)和matchparent/fillparent,對應模式中的MeasureSpec.EXACTLY

包裹內容(width=wrapcontent)則對應模式中的MeasureSpec.AT_MOST

系統呼叫measure方法,從父控制元件到子控制元件的heightMeasureSpec的傳遞是有一套對應的判斷規則的,列表如下:


一個view的寬高尺寸,只有在測量之後才能得到,也就是measure方法被呼叫之後。大家都應該使用過View.getWidth()和View.getHeight()方法,這兩個方法可以返回view的寬和高,但是它們也不是在一開始就可以得到的,比如oncreate方法中,因為這時候measure方法還沒有被執行,測量還沒有完成,我們可以來作一個簡單的實驗:自定義一個MyView,繼承View類,然後在OnCreate方法中,將其new出來,通過addview方法,新增到現在的佈局中。然後呼叫MyView物件的getWidth()和getHeight()方法,會發現得到的都是0。

onMeasure通過父View傳遞過來的大小和模式,以及自身的背景圖片的大小得出自身最終的大小,然後通過setMeasuredDimension()方法設定給mMeasuredWidth和mMeasuredHeight.

普通View的onMeasure邏輯大同小異,基本都是測量自身內容和背景,然後根據父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據文字的長度,文字的大小,文字行高,文字的行寬,顯示方式,背景圖片,以及父View傳遞過來的模式和大小最終確定自身的大小.

三、ViewGroup的onMeasure

ViewGroup是個抽象類,本身沒有實現onMeasure,但是他的子類都有各自的實現,通常他們都是通過measureChildWithMargins函式或者其他類似於measureChild的函式來遍歷測量子View,被GONE的子View將不參與測量,當所有的子View都測量完畢後,才根據父View傳遞過來的模式和大小來最終決定自身的大小.

在測量子View時,會先獲取子View的LayoutParams,從中取出寬高,如果是大於0,將會以精確的模式加上其值組合成MeasureSpec傳遞子View,如果是小於0,將會把自身的大小或者剩餘的大小傳遞給子View,其模式判定在前面表中有對應關係.

ViewGroup一般都在測量完所有子View後才會呼叫setMeasuredDimension()設定自身大小,如第一張圖所示.

可能看到現在,還是沒搞清楚Android系統通過measure和onmeasure一層層傳遞引數的具體方法。在研究這個問題之前,先來看一下最簡單的helloworld的UI層級關係圖:

為了方便起見,這裡我們使用requestWindowFeature(Window.FEATURE_NO_TITLE);去除標題欄的影響,只看層級關係。

  1. <RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  2.     xmlns:tools="http://schemas.android.com/tools"
  3.     android:layout_width="match_parent"
  4.     android:layout_height="match_parent"
  5.     tools:context="${relativePackage}.${activityClass}">
  6.     <TextView
  7.         android:layout_width="wrap_content"
  8.         android:layout_height="wrap_content"
  9.         android:text="@string/hello_world"/>
  10. </RelativeLayout>

  1. package com.example.hello;  
  2. import android.app.Activity;  
  3. import android.os.Bundle;  
  4. import android.view.Window;  
  5. publicclass MainActivity extends Activity {  
  6.     @Override
  7.     protectedvoid onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         requestWindowFeature(Window.FEATURE_NO_TITLE);  
  10.         setContentView(R.layout.activity_main);  
  11.     }  
  12. }  

UI層級關係圖:


可以發現最簡單的helloworld的層級關係圖是這樣的,最開始是一個PhoneWindow的內部類DecorView,這個DecorView實際上是系統最開始載入的最底層的一個viewGroup,它是FrameLayout的子類,然後載入了一個LinearLayout,然後在這個LinearLayout上載入了一個id為content的FrameLayout和一個ViewStub,這個實際上是原本為ActionBar的位置,由於我們使用了requestWindowFeature(Window.FEATURE_NO_TITLE),於是變成了空的ViewStub;然後在id為content的FrameLayout才載入了我們的佈局XML檔案中寫的RelativeLayout和TextView。

那麼measure方法在系統中傳遞尺寸和模式,必定是從DecorView這一層開始的,我們假定手機螢幕是320*480,那麼DecorView最開始是從硬體的配置檔案中讀取手機的尺寸,然後設定measure的引數大小為320*480,而模式是EXCACTLY,傳遞關係可以由下圖示意:

        

好了,原理將到這裡,下一篇將看到利用onMeasure來測量一個自定義一個ImageView,使其能夠自動填滿螢幕的寬度,且能通過measure測量高度,自適應的調整高度,永遠不出現拉伸/壓縮變形的情況,敬請關注,謝謝。