詳解onMeasure()方法中如何測量一個控件尺寸

http://blog.csdn.net/cyp331203/article/details/45027641


        自定義view/viewgroup要重寫的幾個方法:onMeasure(),onLayout(),onDraw()。(不熟悉的話可以查看專欄的前幾篇文章:Android自定義控件系列二:自定義開關按鈕(一))。


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


        如果只是說要重寫什麼方法有什麼用的話,還是不太清楚。先去源碼中看看爲什麼要重寫onMeasure()方法,這個方法是在哪裏調用的:


一、源碼中的measure/onMeasure方法:


[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
  3.             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
  4. }  

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


[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  2. ...  
  3.   
  4. onMeasure(widthMeasureSpec, heightMeasureSpec);  
  5. ...  
  6.   
  7. }  

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);去除標題欄的影響,只看層級關係。


[html] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. <RelativeLayout xmlns: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.   
  7.     <TextView  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="@string/hello_world" />  
  11.   
  12. </RelativeLayout>  

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.example.hello;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.view.Window;  
  6.   
  7. public class MainActivity extends Activity {  
  8.   
  9.     @Override  
  10.     protected void onCreate(Bundle savedInstanceState) {  
  11.         super.onCreate(savedInstanceState);  
  12.         requestWindowFeature(Window.FEATURE_NO_TITLE);  
  13.         setContentView(R.layout.activity_main);  
  14.     }  
  15. }  



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測量高度,自適應的調整高度,永遠不出現拉伸/壓縮變形的情況,敬請關注,謝謝。


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