自定義View之onMeasure()

本文轉自:http://blog.csdn.net/u012604322/article/details/17093421java

 

能夠說重載onMeasure(),onLayout(),onDraw()三個函數構建了自定義View的外觀形象。再加上onTouchEvent()等重載視圖的行爲,能夠構建任何咱們須要的可感知到的自定義View。android

本節咱們探索自定義View中onMeasure()起到了什麼樣的做用,題外要插的一句是,Activity框架,View框架中大量的on函數基本上都應用到了Template模式,掌握這一模式對於理解這些框架大有裨益。程序員

 

咱們知道,無論是自定義View仍是系統提供的TextView這些,它們都必須放置在LinearLayout等一些ViewGroup中,所以理論上咱們能夠很好的理解onMeasure(),onLayout(),onDraw()這三個函數:1.View自己大小多少,這由onMeasure()決定;2.View在ViewGroup中的位置如何,這由onLayout()決定;3.繪製View,onDraw()定義瞭如何繪製這個View。app

首先咱們看看TextView.java中的onMeasure()源碼:框架

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        ... 

        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {
            if (mLayout != null && mEllipsize == null) {
                des = desired(mLayout);
            }

        ...

        setMeasuredDimension(width, height); 

首先咱們要理解的是widthMeasureSpec, heightMeasureSpec這兩個參數是從哪裏來的?onMeasure()函數由包含這個View的具體的ViewGroup調用,所以值也是從這個ViewGroup中傳入的。這裏我直接給出答案:子類View的這兩個參數,由ViewGroup中的layout_width,layout_height和padding以及View自身的layout_margin共同決定。權值weight也是尤爲須要考慮的因素,有它的存在狀況可能會稍微複雜點。ide

 

瞭解了這兩個參數的來源,還要知道這兩個值的做用。咱們只取heightMeasureSpec做說明。這個值由高32位和低16位組成,高32位保存的值叫specMode,能夠經過如代碼中所示的MeasureSpec.getMode()獲取;低16位爲specSize,一樣能夠由MeasureSpec.getSize()獲取。那麼specMode和specSize的做用有是什麼呢?要想知道這一點,咱們須要知道代碼中的最後一行,全部的View的onMeasure()的最後一行都會調用setMeasureDimension()函數的做用——這個函數調用中傳進去的值是View最終的視圖大小。也就是說onMeasure()中以前所做的全部工做都是爲了最後這一句話服務的。函數

咱們知道在ViewGroup中,給View分配的空間大小並非肯定的,有可能隨着具體的變化而變化,而這個變化的條件就是傳到specMode中決定的,specMode一共有三種可能:oop

MeasureSpec.EXACTLY:父視圖但願子視圖的大小應該是specSize中指定的。佈局

MeasureSpec.AT_MOST:子視圖的大小最可能是specSize中指定的值,也就是說不建議子視圖的大小超過specSize中給定的值。this

MeasureSpec.UNSPECIFIED:咱們能夠隨意指定視圖的大小。

由TextView中源碼也能夠知道這個值的設計意義是爲了根據ViewGroup中具體可以提供的空間大小來指定子View的視圖大小。

 

經過以上這些分析,能夠知道視圖最終的大小由父視圖,子視圖以及程序員根據須要決定,良好的設計通常會根據子視圖的measureSpec設置合適的佈局大小。

 

講到上述這些內容,可能已經瞭解瞭如何去使用onMeasure來設置咱們的視圖的大小,但還有一個疑惑的地方,EXACTLY,AT_MOST,UNSPECIFIED和layout_是如何對應的呢?什麼狀況下對應什麼值呢?

咱們經過以下例子,稍做了解:

<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:gravity="center"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin" >

    <LinearLayout
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:paddingTop="20dp"
        android:layout_marginTop="30dp"
        android:background="@android:color/darker_gray" >
        <com.sean.myview.MyView 
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="10dp"
            android:layout_marginTop="15dp"
            android:background="@android:color/holo_red_light"
            />
    </LinearLayout>

</LinearLayout>

效果圖以下:

package com.sean.myview;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class MyView extends View {

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // TODO Auto-generated method stub
        Log.d("MyView","------", new Throwable());
        int speSize = MeasureSpec.getSize(heightMeasureSpec);
        int speMode = MeasureSpec.getMode(heightMeasureSpec);
        Log.d("MyView", "---speSize = " + speSize + "");
        Log.d("MyView", "---speMode = " + speMode + "");
        if(speMode == MeasureSpec.AT_MOST){
            Log.d("MyView", "---AT_MOST---");
        }
        if(speMode == MeasureSpec.EXACTLY){
            Log.d("MyView", "---EXACTLY---");
        }
        if(speMode == MeasureSpec.UNSPECIFIED){
            Log.d("MyView", "---UNSPECIFIED---");
        }
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), speSize);
    }

}

當前狀況下打印出的log以下:

widthMeasureSpecD/MyView  ( 3506): java.lang.Throwable
D/MyView  ( 3506):     at com.sean.myview.MyView.onMeasure(MyView.java:18)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1411)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1059)
D/MyView  ( 3506):     at android.widget.LinearLayout.onMeasure(LinearLayout.java:590)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1411)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1059)
D/MyView  ( 3506):     at android.widget.LinearLayout.onMeasure(LinearLayout.java:590)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView  ( 3506):     at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureVertical(LinearLayout.java:850)
D/MyView  ( 3506):     at android.widget.LinearLayout.onMeasure(LinearLayout.java:588)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView  ( 3506):     at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
D/MyView  ( 3506):     at com.android.internal.policy.impl.PhoneWindow$DecorView.onMeasure(PhoneWindow.java:2193)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:2212)
D/MyView  ( 3506):     at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1291)
D/MyView  ( 3506):     at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1486)
D/MyView  ( 3506):     at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1181)
D/MyView  ( 3506):     at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4942)
D/MyView  ( 3506):     at android.view.Choreographer$CallbackRecord.run(Choreographer.java:776)
D/MyView  ( 3506):     at android.view.Choreographer.doCallbacks(Choreographer.java:579)
D/MyView  ( 3506):     at android.view.Choreographer.doFrame(Choreographer.java:548)
D/MyView  ( 3506):     at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:762)
D/MyView  ( 3506):     at android.os.Handler.handleCallback(Handler.java:800)
D/MyView  ( 3506):     at android.os.Handler.dispatchMessage(Handler.java:100)
D/MyView  ( 3506):     at android.os.Looper.loop(Looper.java:194)
D/MyView  ( 3506):     at android.app.ActivityThread.main(ActivityThread.java:5391)
D/MyView  ( 3506):     at java.lang.reflect.Method.invokeNative(Native Method)
D/MyView  ( 3506):     at java.lang.reflect.Method.invoke(Method.java:525)
D/MyView  ( 3506):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:833)
D/MyView  ( 3506):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)
D/MyView  ( 3506):     at dalvik.system.NativeStart.main(Native Method)
D/MyView  ( 3506): ---speSize = 940
D/MyView  ( 3506): ---speMode = -2147483648
D/MyView  ( 3506): ---AT_MOST---

查看onMeasure()的調用堆棧,而後查看源碼,能夠知道heightMeasureSpec和widthMeasureSpec的值在ViewRootImpl.java中初始化,而初始化又參考了view和LinearLayout中的屬性參數的設置最終獲得了heightMeasureSpec和widthMeasureSpec的值。

而specMode怎麼對應呢?我先給出設置對應值的地方,

如下代碼皆出自ViewRootImpl.java

            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);//這裏是賦值的代碼
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

從這裏咱們基本上能夠看出了MATCH_PARENT對應於EXACTLY,WRAP_CONTENT對應於AT_MOST,其餘狀況也對應於EXACTLY,它和MATCH_PARENT的區別在於size值不同。如今咱們須要知道這個rootDimension即lp.height對應於什麼。

    private void performTraversals() {
        // cache mView since it is used so much below...
...
        WindowManager.LayoutParams lp = mWindowAttributes;
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                mFallbackEventHandler.setView(view);
                mWindowAttributes.copyFrom(attrs);

這個View就是ViewGroup中的各個子類視圖,這裏咱們用實驗說明,更改MyView中的android:layout_height的值爲"match_parent"。log中輸出變爲了

 

D/MyView  ( 4249): ---speSize = 940
D/MyView  ( 4249): ---speMode = 1073741824
D/MyView  ( 4249): ---EXACTLY---

 

而更改LinearLayout中的這個值對這裏的specMode是沒有影響的。

下面咱們再來經過實驗來驗證specSize的值由那些屬性決定:

    <LinearLayout
        android:layout_width="200dp"
        android:layout_height="300dp"
        android:paddingTop="20dp"
        android:layout_marginTop="30dp"
        android:background="@android:color/darker_gray" >
        <com.sean.myview.MyView 
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingTop="10dp"
            android:layout_marginTop="15dp"
            android:background="@android:color/holo_red_light"
            />
    </LinearLayout>

D/MyView  ( 4959): ---speSize = 530
D/MyView  ( 4959): ---speMode = 1073741824
D/MyView  ( 4959): ---EXACTLY---

先說明一點,xml中用的單位是dp,log中獲得的單位是px,我所使用的機子屏幕密度爲2.0,只須要進行簡單的換算便可px = 2.0 * dp

 

咱們能夠經過控制變量法,逐一改變代碼中的LinearLayout和MyView中的相關屬性值,看看是哪些影響了specSize,這裏我直接給出答案:

530 = 300 * 2.0 - 20 * 2.0 - 15 * 2.0

影響specSize height的因素爲:父視圖的layout_height和paddingTop以及自身的layout_marginTop。可是咱們不要忘記有weight時的影響。

相關文章
相關標籤/搜索