android自定義控件(6)- onMeasure()方法中的MeasureSpec


今天的任務就是詳細研究一下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 {

    @Override
    protected 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,傳遞關系可以由下圖示意:

 

擴展閱讀:

MeasureSpec的三個模式詳解UNSPECIFIED,EXACTLY和AT_MOST 

The basic definition of how a View is sized goes like this:
MeasureSpec.EXACTLY - A view should be exactly this many pixels regardless of how big it actually wants to be.
MeasureSpec.AT_MOST - A view can be this size or smaller if it measures out to be smaller.
MeasureSpec.UNSPECIFIED - A view can be whatever size it needs to be in order to show the content it needs to show.
MeasureSpec.AT_MOST will be applied to views that have been set to WRAP_CONTENT if the parent view is bound in size. For example, your parent View might be bound to the screen size. It's children will be also bound to this size, but it might not be that big. Thus, the parent view will set the MeasureSpec to be AT_MOST which tells the child that it can be anywhere between 0 and screen size. The child will have to make adjustments to ensure that it fits within the bounds that was provided.
In special cases, the bounds do not matter. For example, a ScrollView. In the case of a ScrollView, the height of the child Views are irrelevant. As such, it will supply an UNSPECIFIED to the children Views which tells the children that they can be as tall as they need to be. The ScrollViewwill handle the drawing and placement for them.

翻譯一下(直接用軟件翻譯過來的,不明白的話,下面會通過例子說明):

View的大小基本定義如下:
MeasureSpec.EXACTLY -表示父控件已經確切的指定了子View的大小。
MeasureSpec.AT_MOST - 表示子View具體大小沒有尺寸限制,但是存在上限,上限一般為父View大小。
MeasureSpec.UNSPECIFIED -父控件沒有給子view任何限制,子View可以設置為任意大小。
如果父視圖的大小已經指定 則 MeasureSpec.AT_MOST將應用於已設置為WRAP_CONTENT 的子視圖。 例如,你的父視圖可能綁定到屏幕大小。 它的孩子也會綁定到這個大小,但它可能不是那么大。因此,父視圖將MeasureSpec設置為AT_MOST ,它告訴孩子它可以在0和屏幕之間的任何地方。 孩子必須進行調整,以確保它符合提供的界限。
在特殊情況下,界限無關緊要。 例如,一個ScrollView 。 在ScrollView的情況下,子視圖的高度是不相關的。 因此,它將向孩子提供一個UNSPECIFIED視圖,告訴孩子他們可以像他們需要的一樣高。ScrollView將處理它們的繪圖和放置。

模式 數值 描述
UNSPECIFIED 0 (0x00000000) 父控件沒有給子view任何限制,子View可以設置為任意大小。
EXACTLY 1073741824 (0x40000000) 表示父控件已經確切的指定了子View的大小。
AT_MOST -2147483648 (0x80000000) 表示子View具體大小沒有尺寸限制,但是存在上限,上限一般為父View大小。


例子解析
這里分為兩個部分例子,例子一是父布局是LinearLayout,子布局是LinearLayout,例子二是是父布局是ScrollView,子布局是LinearLayout.

1)例子一解釋EXACTLY和AT_MOST
布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical" >

        <com.example.transdotnavi.widget.NormalDot
            android:id="@+id/dot1"
            android:layout_width="20dp"
            android:layout_height="match_parent"
            android:background="#F7F6F5" >
        </com.example.transdotnavi.widget.NormalDot>

</LinearLayout>

子控件,對於具體解釋可以看下面代碼里的注釋

/**
 * @author Administrator
 * 2016-10-22
 * 
 * 普通的圓點導航器
 * 用於測試父控件是layout,子控件是layout的情況
 * 具體可以看layout.xml,里面定義了一個LinearLayout父布局,和一個NormalDot子布局
 * 測試機器:160dpi 480*800
 */
public class NormalDot extends LinearLayout
{
    public NormalDot(Context context)
    {
        super(context);
    }
    @SuppressLint("NewApi") 
    public NormalDot(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }
    public NormalDot(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

//  MeasureSpec.AT_MOST = -2147483648 [0x80000000];  
//  MeasureSpec.EXACTLY = 1073741824 [0x40000000];
//  MeasureSpec.UNSPECIFIED = 0 [0x0];
    
//  父布局LinearLayout固定width,height是match_parent(其實如果設置為wrap_content它的寬高也是等於屏幕寬高)
//  情況一:
//  當寬或高設為確定值時:即width=20dp,height=30dp,或者為match_parent。它會使用MeasureSpec.EXACTLY測量模式(表示父控件已經確切的指定了子View的大小)
//  情況二:
//  當寬或高設為wrap_content時,它會使用MeasureSpec.AT_MOST測量模式(表示子View具體大小沒有尺寸限制,但是存在上限,上限一般為父View大小)
//  情況三:
//  MeasureSpec.UNSPECIFIED,一般是在特殊情況下出現,如在父布局是ScrollView中才會出現這種測量模式
//  注意:
//  父視圖可能綁定到屏幕大小。 它的孩子也會綁定到這個大小,但它可能不是那么大。
//  因此,父視圖將MeasureSpec設置為AT_MOST ,
//  它告訴孩子它可以在0和屏幕之間的任何地方。 孩子必須進行調整,以確保它符合提供的界限。
//  通過一個例子解釋上面面這句化的意思
//  父布局寬高都設置為match_parent,這時候父布局大小就是屏幕大小,這時候,他的子視圖設置為width=20dp,height=wrap_content
//  看打出了的日志可以看到寬是20,高是800,這樣的話我們應該能在屏幕的左邊看到一條白色的寬為20的豎線,
//  但是事實上,我們在界面是沒有看到這條豎線的,這是因為子布局可以占據0到屏幕大小這個范圍,但是子布局通過調整,將height設置為0了,所以
//  我們看不到任何圖像
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int mode2 = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        Log.i("lgy", "mode:"+mode+" width:"+width);
        Log.i("lgy", "mode2:"+mode2+" height:"+height);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

2)例子二 解析UNSPECIFIED模式
布局文件

<?xml version="1.0" encoding="utf-8"?>
<com.example.transdotnavi.widget.LScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.example.transdotnavi.widget.NormalDot2
        android:id="@+id/dot2"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="#F7F6F5" >
<!-- 
        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView" />
             -->
    </com.example.transdotnavi.widget.NormalDot2>

</com.example.transdotnavi.widget.LScrollView>

子控件

/**
 * @author Administrator
 * 2016-10-22
 * 
 * 普通的圓點導航器
 * 用於測試父控件是ScrollView,子控件是layout的情況
 * 具體可以看layout2.xml,里面定義了一個ScrollView父布局,和一個NormalDot2子布局
 * 測試機器:160dpi 480*800
 */
public class NormalDot2 extends LinearLayout
{

    public NormalDot2(Context context)
    {
        super(context);
    }

    /**
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    @SuppressLint("NewApi") 
    public NormalDot2(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        // TODO Auto-generated constructor stub
    }

    /**
     * @param context
     * @param attrs
     */
    public NormalDot2(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        // TODO Auto-generated constructor stub
    }

//  MeasureSpec.AT_MOST = -2147483648 [0x80000000];  
//  MeasureSpec.EXACTLY = 1073741824 [0x40000000];
//  MeasureSpec.UNSPECIFIED = 0 [0x0];

//  這里主要展示的是MeasureSpec.UNSPECIFIED測量模式
//  情況一:垂直的ScrollView
//  父布局ScrollView寬高都是match_parent,子布局是一個NormalDot2設置寬高都是50dp
//  從打出的日志可以看到,寬是以MeasureSpec.EXACTLY模式測量的,width=50,
//  而高是以MeasureSpec.UNSPECIFIED模式測量的,height=0
//  這時候如果NormalDot2布局里沒有任何控件,那么就不會顯示任何東西
//  但如果在里面加個TextView,那么這個textView就會顯示出來(當然這個textView是有內容的,否則也不會顯示出來),
//  但這時候height還是以MeasureSpec.UNSPECIFIED模式測量,height=0
    
//  情況二:水平的ScrollView
//  父布局HorizontalScrollView寬高都是match_parent,子布局是一個NormalDot2設置寬高都是50dp
//  從打出的日志可以看到,寬是MeasureSpec.UNSPECIFIED模式測量的,width=0,
//  而高是以MeasureSpec.EXACTLY模式測量的,height=50
//  這時候如果NormalDot2布局里沒有任何控件,那么就不會顯示任何東西
//  但如果在里面加個TextView,那么這個textView就會顯示出來(當然這個textView是有內容的,否則也不會顯示出來),
//  但這時候width還是以MeasureSpec.UNSPECIFIED模式測量,width=0 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int mode2 = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        Log.i("lgy", "============mode:"+mode+" width:"+width);
        Log.i("lgy", "=============mode2:"+mode2+" height:"+height);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

父控件(ScrollView)

/**
 * @author LGY
 * @time 2016-10-23
 * @action 
 */
public class LScrollView extends 
HorizontalScrollView
//ScrollView
{

    /**
     * @param context
     */
    public LScrollView(Context context)
    {
        super(context);
        // TODO Auto-generated constructor stub
    }

    /**
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public LScrollView(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        // TODO Auto-generated constructor stub
    }

    /**
     * @param context
     * @param attrs
     */
    public LScrollView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        // TODO Auto-generated constructor stub
    }
    
    /* (non-Javadoc)
     * @see android.widget.ScrollView#onMeasure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int mode2 = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        Log.i("lgy", "ScrollViewmode:"+mode+" width:"+width);
        Log.i("lgy", "ScrollViewmode2:"+mode2+" height:"+height);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

4.源碼地址
http://download.csdn.net/detail/lgywsdy/9757471

5.參考文章
http://stackoverflow.com/questions/16022841/when-will-measurespec-unspecified-and-measurespec-at-most-be-applied

 

參考鏈接

[Android][MeasureSpec的三個測量模式]

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM