Android自訂控制項系列七:詳解onMeasure()方法中如何測量一個控制項尺寸(一),androidonmeasure
轉載請註明出處:http://blog.csdn.net/cyp331203/article/details/45027641
自訂view/viewgroup要重寫的幾個方法:onMeasure(),onLayout(),onDraw()。(不熟悉的話可以查看專欄的前幾篇文章:Android自訂控制項系列二:自訂開關按鈕(一))。
今天的任務就是詳細研究一下protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。
如果只是說要重寫什麼方法有什麼用的話,還是不太清楚。先去源碼中看看為什麼要重寫onMeasure()方法,這個方法是在哪裡調用的:
一、源碼中的measure/onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
實際上是在View這個類中的public final void measure(int widthMeasureSpec, int heightMeasureSpec)方法中被調用的:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {...onMeasure(widthMeasureSpec, heightMeasureSpec);...}
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);去除標題列的影響,只看層級關係。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="${relativePackage}.${activityClass}" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" /></RelativeLayout>
package com.example.hello;import android.app.Activity;import android.os.Bundle;import android.view.Window;public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);requestWindowFeature(Window.FEATURE_NO_TITLE);setContentView(R.layout.activity_main);}}
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測量高度,自適應的調整高度,永遠不出現展開/壓縮變形的情況,敬請關注,謝謝。
轉載請註明出處:http://blog.csdn.net/cyp331203/article/details/45027641