反思|Android View機制設計與實現:測量流程

反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏html

概述

Android自己的View體系很是宏大,源碼中值得思考和借鑑之處衆多,以View自己的繪製流程爲例,其通過measure測量、layout佈局、draw繪製三個過程,最終纔可以將其繪製出來並展現在用戶面前。android

本文將針對繪製過程當中的 測量流程 的設計思想進行系統地概括總結,讀者須要對Viewmeasure()相關知識有初步的瞭解:git

總體思路

View的測量機制本質很是簡單,顧名思義,其目的即是 測量控件的寬高值,圍繞該目的,View的設計者經過代碼編織了一整套複雜的邏輯:github

  • 一、對於子View而言,其自己寬高直接受限於父View佈局要求,舉例來講,父View被限制寬度爲40px,子View的最大寬度一樣也需受限於這個數值。所以,在測量子View之時,子View必須已知父View的佈局要求,這個 佈局要求Android中經過使用 MeasureSpec 類來進行描述。安全

  • 二、對於完整的測量流程而言,父控件必然依賴子控件寬高的測量;若子控件自己未測量完畢,父控件自身的測量亦無從談起。AndroidView的測量流程中使用了很是經典的 遞歸思想:對於一個完整的界面而言,每一個頁面都映射了一個View樹,其最頂端的父控件測量開始時,會經過 遍歷 將其 佈局要求 傳遞給子控件,以開始子控件的測量,子控件在測量過程當中也會經過 遍歷 將其 佈局要求 傳遞給它本身的子控件,如此往復一直到最底層的控件...這種經過遍歷自頂向下傳遞數據的方式咱們稱爲 測量過程當中的「遞」流程。而當最底層位置的子控件自身測量完畢後,其父控件會將全部子控件的寬高數據進行聚合,而後經過對應的 測量策略 計算出父控件自己的寬高,測量完畢後,父控件的父控件也會根據其全部子控件的測量結果對自身進行測量,這種從底部向上傳遞各自的測量結果,最終完成最頂層父控件的測量方式咱們稱爲測量過程當中的「歸」流程,至此界面整個View樹測量完畢。app

對於繪製流程不甚熟悉的開發者而言,上述文字彷佛晦澀難懂,但這些文字的歸納其本質倒是繪製流程總體的設計思想,讀者不該該將本文視爲源碼分析,而應該將本身代入到設計的過程當中 ,當深入理解整個流程的設計思路以後,測量流程代碼地設計和編寫天然行雲流水一鼓作氣。函數

佈局要求

在整個 測量流程 中, 佈局要求 都是一個很是重要的核心名詞,Android中經過使用 MeasureSpec 類來對其進行描述。源碼分析

爲何說 佈局要求 很是重要呢,其又是如何定義的呢?這要先從結果提及,對於單個View來講,測量流程的結果無非是獲取控件自身寬和高的值,Android提供了setMeasureDimension()函數,開發者僅須要將測量結果做爲參數並調用該函數,即可以視爲View完成了自身的測量:佈局

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
 // measuredWidth 測量結果,View的寬度
 // measuredHeight 測量結果,View的高度
 // 省略其它代碼...

 // 該方法的本質就是將測量結果存起來,以便後續的layout和draw流程中獲取控件的寬高
 mMeasuredWidth = measuredWidth;
 mMeasuredHeight = measuredHeight;
}
複製代碼

須要注意的是,子控件的測量過程自己還應該依賴於父控件的一些佈局約束,好比:post

  • 1.父控件固定寬高只有${x}px,子控件設置爲layout_height="${y}px";
  • 2.父控件高度爲wrap_content(包裹內容),子控件設置爲layout_height="match_parent";
  • 3.父控件高度爲match_parent(填充),子控件設置爲layout_height="match_parent";

這些狀況下,由於沒法計算出準確控件自己的寬高值,簡單的經過setMeasuredDimension()函數彷佛不可能達到測量控件的目的,由於 子控件的測量結果是由父控件和其自己共同決定的 (這個下文會解釋),而父控件對子控件的佈局約束,即是前文提到的 佈局要求,即MeasureSpec類。

MeasureSpec類

從面向對象的角度來看,咱們將MeasureSpec類設計成這樣:

public final class MeasureSpec {

  int size;     // 測量大小
  Mode mode;    // 測量模式

  enum Mode { UNSPECIFIED, EXACTLY, AT_MOST }

  MeasureSpec(Mode mode, int size){
    this.mode = Mode;
    this.size = size;
  }

  public int getSize() { return size; }

  public Mode getMode() { return mode; }
}
複製代碼

在設計的過程當中,咱們將佈局要求分紅了2個屬性。測量大小 意味着控件須要對應大小的寬高,測量模式 則表示控件對應的寬高模式:

  • UNSPECIFIED:父元素不對子元素施加任何束縛,子元素能夠獲得任意想要的大小;平常開發中自定義View不考慮這種模式,可暫時先忽略;
     * EXACTLY:父元素決定子元素的確切大小,子元素將被限定在給定的邊界裏而忽略它自己大小;這裏咱們理解爲控件的寬或者高被設置爲 match_parent 或者指定大小,好比20dp
  • AT_MOST:子元素至多達到指定大小的值;這裏咱們理解爲控件的寬或者高被設置爲wrap_content

巧妙的是,Android並不是經過上述定義MeasureSpec對象的方式對 佈局要求 進行描述,而是使用了更簡單的二進制的方式,用一個32位的int值進行替代:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30; //移位位數爲30
    //int類型佔32位,向右移位30位,該屬性表示掩碼值,用來與size和mode進行"&"運算,獲取對應值。
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    //00左移30位,其值爲00 + (30位0)
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //01左移30位,其值爲01 + (30位0)
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //10左移30位,其值爲10 + (30位0)
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    // 根據size和mode,建立一個測量要求
    public static int makeMeasureSpec(int size, int mode) {
        return size + mode;
    }

    // 根據規格提取出mode,
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    // 根據規格提取出size
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}
複製代碼

這個int值中,前2位表明了測量模式,後30位則表示了測量的大小,對於模式和大小值的獲取,只須要經過位運算便可。

以寬度舉例來講,若咱們設置寬度=5px(二進制對應了101),那麼mode對應EXACTLY,在建立測量要求的時候,只須要經過二進制的相加,即可獲得存儲了相關信息的int值:

而當須要得到Mode的時候只須要用measureSpecMODE_TASK相與便可,以下圖:

同理,想得到size的話只須要只須要measureSpec~MODE_TASK相與便可,以下圖:

如今讀者對MeasureSpec類有了初步地認識,在Android繪製過程當中,View寬或者高的 佈局要求 其實是經過32位的int值進行的描述, 而MeasureSpec類自己只是一個靜態方法的容器而已。

至此MeasureSpec類所表明的 佈局要求 已經介紹完畢,這裏咱們淺嘗輒止,其在後文的 總體測量流程 中佔有相當重要的做用,屆時咱們再進行對應的引伸。

測量單個控件

只考慮單個控件的測量,整個過程須要定義三個重要的函數,分別爲:

  • final void measure(int widthMeasureSpec, int heightMeasureSpec):執行測量的函數;
  • void onMeasure(int widthMeasureSpec, int heightMeasureSpec):真正執行測量的函數,開發者須要本身實現自定義的測量邏輯;
  • final void setMeasuredDimension(int measuredWidth, int measuredHeight):完成測量的函數;

爲何說須要定義這樣三個函數?

1.measure()入口函數:標記測量的開始

首先父控件須要經過調用子控件的measure()函數,並同時將寬和高的 佈局要求 做爲參數傳入,標誌子控件自己測量的開始:

// 這個是父控件的代碼,讓子控件開始測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
複製代碼

對於View的測量流程,其必然包含了2部分:公共邏輯部分開發者自定義測量的邏輯部分,爲了保證公共邏輯部分代碼的安全性,設計者將measure()方法配置了final修飾符:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  // ... 公共邏輯

  // 開發者須要本身重寫onMeasure函數,以自定義測量邏輯
  onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製代碼

開發者不能重寫measure()函數,並將View自定義測量的策略經過定義一個新的onMeasure()接口暴露出來供開發者重寫。

2.onMeasure()函數:自定義View的測量策略

onMeasure()函數中,View自身也提供了一個默認的測量策略:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼

以寬度爲例,經過這樣獲取View默認的寬度:

getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)

  • 1.在某些狀況下(好比自身設置了minWidth或者background屬性),View須要經過getSuggestedMinimumWidth()函數做爲默認的寬度值:
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製代碼
  • 2.這以後,將所得結果做爲參數傳遞到getDefaultSize(minWidth, widthMeasureSpec)函數中,根據 佈局要求 計算出View最後測量的寬度值:
public static int getDefaultSize(int size, int measureSpec) {
    // 寬度的默認值
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    // 根據不一樣的測量模式,返回的測量結果不一樣
    switch (specMode) {
      // 任意模式,寬度爲默認值
      case MeasureSpec.UNSPECIFIED:
          result = size;
          break;
      // match_parent、wrap_content則返回佈局要求中的size值
      case MeasureSpec.AT_MOST:
      case MeasureSpec.EXACTLY:
          result = specSize;
          break;
    }
    return result;
}
複製代碼

上述代碼中,View的默認測量策略也印證了,即便View設置的是layout_width="wrap_content",其寬度也會填充父佈局(效果同match_parent),高度依然。

3.setMeasuredDimension()函數:標誌測量的完成

setMeasuredDimension(width,height)函數的存在乎義很是重要,在onMeasure()執行自定義測量策略的過程當中,調用該函數標誌着View的測量得出告終果:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 廣泛意義上,setMeasuredDimension()標誌着測量結束
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
 // measuredWidth 測量結果,View的寬度
 // measuredHeight 測量結果,View的高度
 // 省略其它代碼...

 // 該方法的本質就是將測量結果存起來,以便後續的layout和draw流程中獲取控件的寬高
 mMeasuredWidth = measuredWidth;
 mMeasuredHeight = measuredHeight;
}
複製代碼

該函數被設計爲由protected final修飾,這意味着只能由子類進行調用而不能重寫。

函數調用完畢,開發者能夠經過getMeasuredWidth()或者getMeasuredHeight()來獲取View測量的寬高,代碼設計大概是這樣:

public final int getMeasuredWidth() {
    return mMeasuredWidth;
}
public final int getMeasuredHeight() {
    return mMeasuredHeight;
}

// 如何使用
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight()
複製代碼

通過measure() -> onMeasure() -> setMeasuredDimension()函數的調用,最終View自身測量流程執行完畢。

完整測量流程

對於一個完整的界面而言,每一個頁面都映射了一個View樹,見微知著,瞭解了單個View的測量過程,從宏觀的角度思考,View樹總體的測量流程將如何實現?

一、設計思路

首先須要理解的是,每種ViewGroup的子類的測量策略(也就是onMeasure()函數內的邏輯)不盡相同,好比RelativeLayout或者LinearLayout寬高的測量策略天然不一樣,但總體思路都大同小異,即 遍歷 測量全部子控件,根據父控件自身測量策略進行寬高的計算並得出測量結果。

豎直方向佈局LinearLayout爲例,如何完成LinearLayout高度的測量?本文拋去不重要的細節,化繁爲簡,將LinearLayout高度的測量策略簡單定義爲 遍歷獲取全部子控件,將高度累加 ,所得值即自身高度的測量結果——若是不知道每一個子控件的高度,LinearLayout天然沒法測量出自己的高度。

所以對於View樹總體的測量而言,控件的測量其實是 自底向上 的,正如文章開篇 總體思路 一節所描述的:

對於完整的測量流程而言,父控件必然依賴子控件寬高的測量;若子控件自己未測量完畢,父控件自身的測量亦無從談起。

此外,由於子控件的測量邏輯受限於父控件傳過來的 佈局要求(MeasureSpec), 所以總體邏輯應該是:

    1. 測量開始時,由頂層的父控件將佈局要求傳遞給子控件,以通知子控件開始執行測量;
    1. 子控件根據測量策略計算出自身的佈局要求,再傳遞給下一級的子控件,通知子控件開始測量,如此往復,直至到達最後一級的子控件;
    1. 最後一級的子控件測量完畢後,執行setMeasuredDimension()函數,其父控件根據本身的測量策略,將全部child的寬高和佈局屬性進行對應的計算(好比上文中LinearLayout就是計算全部子控件高度的和),獲得本身自己的測量寬高;
    1. 該控件經過調用setMeasuredDimension()函數完成測量,這以後,它的父控件再根據其自身測量策略完成測量,如此往復,直至完成頂層級View的測量,自此,整個頁面測量完畢。

這裏的設計體現出了經典的 遞歸思想,一、2步驟,開始測量的通知自頂至下,咱們稱之爲測量步驟的 遞流程,三、4步驟,測量完畢的順序倒是自底至頂,咱們稱之爲測量步驟的 歸流程

二、遞流程的實現

如今根據上一小節的設計思路,開始對 遞流程 進行編碼實現。

在整個遞流程中,MeasureSpec所表明的 佈局要求 佔有相當重要的做用,瞭解了它在這個過程當中的意義,也就理解了爲何咱們常說 子控件的測量結果是由父控件和其自己共同決定的

依然以 豎直方向佈局LinearLayout爲例,咱們須要遍歷測量其全部的子控件,所以,在onMeasure()函數中,第一次咱們編碼以下:

// 1.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  // 1.經過遍歷,對每一個child進行測量
  for(int i = 0 ; i < getChildCount() ; i++){  
    View child = getChildAt(i);
    // 2.直接測量子控件
    child.measure(widthMeasureSpec, heightMeasureSpec);
  }
  // ...
  // 3.全部子控件測量完畢...
  // ...
}
複製代碼

這裏關注int heightMeasureSpec參數,咱們知道,這個32位int類型的值,包含了父佈局傳過來高度的 佈局要求:測量的大小和模式。如今咱們思考,若父佈局傳過來大小的是屏幕的高度,那麼將其做爲參數直接執行child.measure(widthMeasureSpec, heightMeasureSpec),讓子控件直接開始測量,是合理的嗎?

答案固然是否認的,試想這樣一個簡單的場景,若LinearLayout自己設置了padding值,那麼子控件的最大高度便不能再達到heightMeasureSpec中size的大小了,可是若是像上述代碼中的步驟2同樣,直接對子控件進行測量,子控件就能夠從heightMeasureSpec參數中取得屏幕的高度,經過setMeasuredDimension()將本身的高度設置和父控件高度一致——這致使了padding值配置的失效,並不符合預期。

所以,咱們須要額外設計一個可重寫的函數,用於自定義對child的測量:

protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
		// 獲取子元素的佈局參數
    final LayoutParams lp = child.getLayoutParams();
    // 經過padding值,計算出子控件的佈局要求
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    // 將新的佈局要求傳入measure方法,完成子控件的測量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼

咱們定義了measureChild()函數,其做用是計算子控件的佈局要求,並把新的佈局要求傳給子控件,再讓子控件根據新的佈局要求進行測量,這樣就解決了上述的問題,由此也說明了爲何 子控件的測量結果是由父控件和其自己共同決定的

這裏咱們注意到咱們設計了一個getChildMeasureSpec()函數,那麼這個函數是作什麼的呢?

getChildMeasureSpec()函數

getChildMeasureSpec()函數的做用是根據父佈局的MeasureSpecpadding值,計算出對應子控件的MeasureSpec,由於這個函數的邏輯是能夠複用的,所以將其定義爲一個靜態函數:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //獲取父View的測量模式
    int specMode = MeasureSpec.getMode(spec);
    //獲取父View的測量大小
    int specSize = MeasureSpec.getSize(spec);
    //父View計算出的子View的大小,子View不必定用這個值
    int size = Math.max(0, specSize - padding);
    //聲明變量用來保存實際計算的到的子View的size和mode即大小和模式
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
    //若是父容器的模式是Exactly即肯定的大小
    case MeasureSpec.EXACTLY:
    	//子View的高度或寬度>0說明其實一個確切的值,由於match_parent和wrap_content的值是<0的
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
            //子View的高度或寬度爲match_parent
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;//將size即父View的大小減去邊距值所獲得的值賦值給resultSize
            resultMode = MeasureSpec.EXACTLY;//指定子View的測量模式爲EXACTLY
           //子View的高度或寬度爲wrap_content
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;//將size賦值給result
            resultMode = MeasureSpec.AT_MOST;//指定子View的測量模式爲AT_MOST
        }
        break;
    //若是父容器的測量模式是AT_MOST
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            // 由於父View的大小是受到限制值的限制,因此子View的大小也應該受到父容器的限制而且不能超過父View 
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    //若是父容器的測量模式是UNSPECIFIED即父容器的大小未受限制
    case MeasureSpec.UNSPECIFIED:
    	//若是自View的寬和高是一個精確的值
        if (childDimension >= 0) {
        	  //子View的大小爲精確值
            resultSize = childDimension;
            //測量的模式爲EXACTLY
            resultMode = MeasureSpec.EXACTLY;
            //子View的寬或高爲match_parent
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
        	  //由於父View的大小是未定的,因此子View的大小也是未定的
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //根據resultSize和resultMode調用makeMeasureSpec方法獲得測量要求,並將其做爲返回值
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼

邏輯分支相對較多,註釋中已經將子控件 佈局要求 的計算邏輯寫清楚了,總結以下圖,原圖連接

爲何說這個函數很是重要?由於這個函數纔是 子控件的測量結果是由父控件和其自己共同決定的 最直接的體現,同時,在不一樣的佈局模式下(match_parentwrap_content、指定dp/px),其對應子控件的佈局要求的返回值亦不一樣,建議讀者認真理解這段代碼。

回到前文,如今咱們對onMeasure()的方法定義以下:

// 2.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  // 1.經過遍歷,對每一個child進行測量
  for(int i = 0 ; i < getChildCount() ; i++){  
     View child = getChildAt(i);
     // 2.計算新的佈局要求,並對子控件進行測量
     measureChild(child, widthMeasureSpec, heightMeasureSpec);
  }
  // ...
  // 3.全部子控件測量完畢...
  // ...
}
複製代碼

三、歸流程的實現

如今,全部子控件測量完畢,接下來 歸流程 的實現就很簡單了,將全部childheight進行累加,並調用 setMeasuredDimension()結束測量便可:

// 3.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  // 1.經過遍歷,對每一個child進行測量
  for(int i = 0 ; i < getChildCount() ; i++){  
     View child = getChildAt(i);
     // 2.計算新的佈局要求,並對子控件進行測量
     measureChild(child, widthMeasureSpec, heightMeasureSpec);
  }
  // 3.完成子控件的測量,對高度進行累加
  int height = 0;
  for(int i = 0 ; i < getChildCount() ; i++){  
      height += child.getMeasuredHeight();  
  }
  // 4.完成LinearLayout的測量
  setMeasuredDimension(width, height);
}
複製代碼

乍一看,彷佛很難體現出整個流程的 遞歸 性,實際上當咱們宏觀從View樹的樹頂順着往下整理思路,代碼邏輯的執行順序一目瞭然:

如圖所示,實線表明了測量流程中總體自頂向下的 遞流程, 而虛線表明了自底向上的 歸流程

至此,測量流程總體實現完畢。

參考


關於我

Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 Github

若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章
相關標籤/搜索