Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View measure流程詳解

Android View measure流程詳解

編輯:關於Android編程

Android中View繪制的流程包括:measure(測量)->layout(布局)->draw(繪制).

因為Android中每個View都占據了一塊矩形的空間,當我們要在屏幕上顯示這個矩形的View的時候

首先我們需要知道這個矩形的大小(寬和高)這就對應了View的measure流程. 有了View的寬和高,我們還需要知道View左上角的起點在哪裡,右下角的終點在哪裡,這就對應了View的layout流程. 當矩形的區域在屏幕上確定之後,相當於屏幕上有了一塊屬於View的畫布,接下來通過draw方法就可以在這塊畫布上畫畫了.

本文重點介紹View的measure流程.一般情況下,我們都是在xml文件裡去定義一個布局文件,針對每個View來說,是一定要聲明layout_width和layout_height的,不然編譯期就會報錯.

那layout_width和layout_height數值包括:

具體的長度單位,例如20dp或者20px. match_parent,代表充滿父控件. wrap_content,代表能包含View中內容即可.

從layout_width和layout_height的取值來看,如果Android規定只能使用具體的長度,那可能就不需要measure流程了,因此寬和高已經知道了.因此,從取值上我們知道了measure方法的作用是:將match_parent和wrap_content轉成具體的度量值.


measure方法

接下來,我們從源碼的角度去分析measure方法.

/**
 * 

* 這個方法是用來測試View的寬和高的. View的父附件提供了寬和高的限制參數,即View的最值. *

* *

* View的真正測量工作是在 {@link #onMeasure(int, int)} 函數裡進行的. 因此, 只有 * {@link #onMeasure(int, int)} 方法才能被子類重寫. *

* */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { // 判斷View的layoutMode是否為LAYOUT_MODE_OPTICAL_BOUNDS boolean optical = isLayoutModeOptical(this); // 子View是LAYOUT_MODE_OPTICAL_BOUNDS,父附件不是LAYOUT_MODE_OPTICAL_BOUNDS的情況很少見,不需要去care if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } // 生成View寬高的緩存key,並且如果緩存Map為null,則構建緩存Map. long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); // 判斷是否為強制布局或者寬、高發生了變化 if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // 清除PFLAG_MEASURED_DIMENSION_SET標記,表示該View還沒有被測量. mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; // 解析從右向左的布局 resolveRtlPropertiesIfNeeded(); // 判斷是否能用cache緩存中view寬和高 int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // View寬和高真正測量的地方 onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { // 獲取緩存中的View寬和高 long value = mMeasureCache.valueAt(cacheIndex); // long占8個字節,前4個字節為寬度,後4個字節為高度. setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // 無論是調用onMeasure還是使用緩存,都應該設置了PFLAG_MEASURED_DIMENSION_SET標志位. // 沒有設置,則說明測量過程出了問題,因此拋出異常. // 並且,一般出現這種情況一般是子類重寫onMeasure方法,但是最後沒有調用setMeasuredDimension. if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } // 記錄View的寬和高,並將其存儲到緩存Map裡. mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); }

從measure方法中,我們可以看出,measure方法只要是加了一個緩存機制,真正的測量還是在onMeasure去執行.為了防止緩存的濫用,Android系統直接將measure設置為final類型,即子類不能重寫,意味著子類只需要提供測量的具體實現,不需要care緩存等提速功能的實現.

onMeasure方法

onMeasure的默認實現非常簡單,注釋源碼如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 將getDefaultSize函數處理後的值設置給寬和高的成員變量
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    // 特殊處理LAYOUT_MODE_OPTICAL_BOUNDS的情況
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }

    // 真正設置View寬和高的地方
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    // 保存寬度到View的mMeasuredWidth成員變量中
    mMeasuredWidth = measuredWidth;
    // 保存高度到View的mMeasuredHeight成員變量中
    mMeasuredHeight = measuredHeight;
    // 設置View的PFLAG_MEASURED_DIMENSION_SET,即代表當前View已經被測量過.
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

通過源碼,我們發現onMeasure只是給mMeasuredWidth和mMeasuredHeight兩個成員變量賦值.那這個值是如何獲取到的呢?和Parent提供的限制又有神馬關系呢?
想回答這個問題,需要先跟蹤一下getDefaultSize的源碼.

public static int getDefaultSize(int size, int measureSpec) {
    // size表示View想要的尺寸信息,比如最小寬度或者最小高度
    int result = size;
    // 解析SpecMode信息
    int specMode = MeasureSpec.getMode(measureSpec);
    // 解析SpecSize信息
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        // 如果View的度量值設置為WARP_CONTENT的時候
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        // 如果View的度量值設置為具體數值或者MATCH_PARENT的時候
        result = specSize;
        break;
    }
    return result;
}

同時,需要看一下getSuggested一系列方法,以getSuggestedMinimumWidth為例:

protected int getSuggestedMinimumWidth() {
    // 如果View沒有設置背景,則返回View本身的最小寬度mMinWidth.
    // 如果View設置了背景,那麼就取mMinWidth和背景寬度的最大值.
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

這裡有同學會奇怪View的mMinWidth(最小寬度)是什麼時候設置的?我們可以看一下View的構造函數(截取部分代碼):

case R.styleable.View_minWidth:
    mMinWidth = a.getDimensionPixelSize(attr, 0);
    break;

可以看到,和我們自定義一個View是一樣的,mMinWidth成員變量對應著的自定義屬性是minWidth,如果xml中未定義則默認值是0.
示例設置代碼:

或許還有同學會對上面的MeasureSpec感到陌生,不要著急,這就帶來MeasureSpec的詳解.

MeasureSpec

MeasureSpec的值由specSize和spceMode共同組成,其中specSize記錄的是大小,specMode記錄的是規格.
我們來看一下MeasureSpec的源碼定義,他是View的內部類,源碼位於: /frameworks/base/core/java/android/view/View.java

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;

    // specMode一共有三種類型
    public static final UNSPECIFIED = 0 << MODE_SHIFT;  // 任意大小
    public static final int EXACTLY = 1 << MODE_SHIFT;  // 表示父容器希望子視圖的大小由specSize來決定
    public static final int AT_MOST = 2 << MODE_SHIFT;  // 表示子視圖最多只能是specSize中指定的大小

    public static int makeMeasureSpec(int size, int mode) {
        return size + mode;
    }

    public static int getMode(int measureSpec) {
        return measureSpec & MODE_MASK;
    }

    public static int getSize(int measureSpec) {
        return measureSpec & ~MODE_MASK;
    }
}

只看源碼可能大家還比較陌生,這裡結合源碼講解一下.首先,Android只所以提供MeasureSpec類,我認為Android開發人員覺得View尺寸最大不超過2^30,所以將剩余的2位來表示模式,這樣節約了空間.
前面說過,View的寬和高只有三種情況,分別是:具體數值,WARP_CONTENT,MATCH_PARENT.而measure就是將WARP_CONTENT,MATCH_PARENT轉換為具體數值的方法,所以需要有MeasureSpec.getSize()方法獲取具體數值.
那MeasureSpec.getMode的幾種類型和View的賦值也是有對應關系的:

EXACTLY: 對應著具體數值或者MATCH_PARENT AT_MOST: 對應著WRAP_CONTENT UNSPECIFIED: 父容器對View無限制,這中情況一般出現在Android系統內部.

ViewGroup的onMeasure流程

對於非ViewGroup的View,上面介紹的measure->onMeasure方法足以完成View的測量.
但是對於自定義ViewGroup,我們不僅要測量自己的寬和高,同時還需要負責測量child view的寬和高.

測量child view通常會用到measureChild方法.源碼實現如下:

/**
 * 測量每個子View的寬高.測量時需要排除padding的影響,如果需要排除margins,則調用measureChildWithMargins方法.
 */
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看到,measureChild方法只要是獲取子View的widthMeasureSpec和heightMeasureSpec,然後調用measure方法設置子View的實際寬高值.同時,從getChildMeasureSpec的方法傳遞中,我們可以看出child View的值是由父View和child view共同決定的.

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 獲取父容器的MeasureSpecMode
    int specMode = MeasureSpec.getMode(spec);
    // 獲取父容器的具體大小
    int specSize = MeasureSpec.getSize(spec);
    // 子view的可能大小=父View的大小-padding
    int size = Math.max(0, specSize - padding);

    // 設置子View的初始MeasureSpecSize和MeasureSpecMode均為0
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // 當父容器有固定大小(具體數值或MATCH_PARENT)的情況下
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            // view的寬或高賦值為具體數值,例如20dp
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // view的寬或高賦值為MATCH_PARENT
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // view的寬或高賦值為WRAP_CONTENT,這時View的最大值就是父容器大小-padding.所以MeasureSpecSize=父容器大小-padding.
            // 同時,View的最大值為MeasureSpecSize,所以MeasureSpecMode設置為AT_MOST
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 當父容器是AT_MOST情況下(即父容器大小賦值為WRAP_CONTENT)
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 由於父容器的mode為AT_MOST,雖然子View設置為MATCH_PARENT,但是父容器大小不是精確的,所以子View也只能是AT_MOST了.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 這個case不需要care,自定義控件不會遇到這種case的.
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

getChildMeasureSpec的總體思路:
通過其父容器提供的MeasureSpec參數得到specMode和specSize,並根據計算出來的specMode以及子視圖的childDimension(layout_width和layout_height中定義的值)來計算子View自身的measureSpec和measureMode.

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved