1. 程式人生 > >Android View的繪製流程

Android View的繪製流程

本文主要是梳理 View 繪製的整體流程,幫助開發者對 View 的繪製有一個更深層次的理解。

整體流程

View 繪製中主要流程分為measure,layout, draw 三個階段。

measure :根據父 view 傳遞的 MeasureSpec 進行計算大小。

layout :根據 measure 子 View 所得到的佈局大小和佈局引數,將子View放在合適的位置上。

draw :把 View 物件繪製到螢幕上。

那麼發起繪製的入口在哪裡呢?

在介紹發起繪製的入口之前,我們需要先了解Window,ViewRootImpl,DecorView之間的聯絡。

一個 Activity 包含一個Window,Window是一個抽象基類,是 Activity 和整個 View 系統互動的介面,只有一個子類實現類PhoneWindow,提供了一系列視窗的方法,比如設定背景,標題等。一個PhoneWindow 對應一個 DecorView 跟 一個 ViewRootImpl,DecorView 是ViewTree 裡面的頂層佈局,是繼承於FrameLayout,包含兩個子View,一個id=statusBarBackground 的 View 和 LineaLayout,LineaLayout 裡面包含 title 跟 content,title就是平時用的TitleBar或者ActionBar,contenty也是 FrameLayout,activity通過 setContent()載入佈局的時候載入到這個View上。ViewRootImpl 就是建立 DecorView 和 Window 之間的聯絡。

這三個階段的核心入口是在 ViewRootImpl 類的 performTraversals() 方法中。

private void performTraversals() {
    ......
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ......
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
    mView.draw(canvas);
    ......
 }

在原始碼中這個方法賊長,但是核心還是這三個步驟,就是判斷根據之前的狀態判斷是否需要重新 measure,是否需要重新 layout ,是否需要重新 draw。

measurespeac

在介紹 measure 方法之前,需要了解一個很核心的概念:measureSpeac 。在 Google 官方文件中是這麼定義 measureSpeac

A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.

大概意思是:MeasureSpec 封裝了從父View 傳遞給到子View的佈局需求。每個MeasureSpec代表寬度或高度的要求。每個MeasureSpec都包含了size(大小)和mode(模式)。

我覺得這是measureSpeac 最好的解釋了。

後面兩句不難理解。MeasureSpec 一個32位二進位制的整數型,前面2位代表的是mode,後面30位代表的是size。mode 主要分為3類,分別是

  • EXACTLY:父容器已經測量出子View的大小。對應是 View 的LayoutParams的match_parent 或者精確數值。

  • AT_MOST:父容器已經限制子view的大小,View 最終大小不可超過這個值。對應是 View 的LayoutParams的wrap_content

  • UNSPECIFIED:父容器不對View有任何限制,要多大給多大,這種情況一般用於系統內部,表示一種測量的狀態。(這種不怎麼常用,下面分析也會直接忽略這種情況)

封裝了從父 View 傳遞給到子 View 的佈局需求,這句話又怎麼理解呢?

View 的 MeasureSpec 並不是父 View 獨自決定,它是根據父 view 的MeasureSpec加上子 View 的自己的 LayoutParams,通過相應的規則轉化。

看程式碼:

View 測量流程是父 View 先測量子 View,等子 View 測量完了,再來測量自己。在ViewGroup 測量子 View 的入口就是 measureChildWithMargins

 protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {

    //獲取子View的LayoutParam
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //通過父View的MeasureSpec和子View的margin,父View的padding計算,算出子View的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    //通過計算出來的MeasureSpec,讓子View自己測量。
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}


public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //計運算元View的大小
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // 父View是EXACTLY的
    case MeasureSpec.EXACTLY:
        //子View的width或height是個精確值,則size為精確值,mode為 EXACTLY
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        //子View的width或height是MATCH_PARENT,則size為父檢視大小,mode為 EXACTLY
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        //子View的width或height是WRAP_CONTENT,則size為父檢視大小,mode為 AT_MOST
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 2、父View是AT_MOST的
    case MeasureSpec.AT_MOST:
        //子View的width或height是個精確值,則size為精確值,mode為 EXACTLY
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        //子View的width或height是MATCH_PARENT,則size為父檢視大小,mode為 AT_MOST
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        //子View的width或height是MATCH_PARENT,則size為父檢視大小,mode為 AT_MOST
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 父View是UNSPECIFIED的
    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;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

通過程式碼我們很可以很看到 View 的子 View 的 MeasureSpec 轉化規則,但是感覺可能有點懵,我們用一個”商城-衣服”例子來比喻一下:

我們把父 View 看做成商城,子 View 看做成衣服,EXACTLY / MATCH_PARENT 看做成高檔品牌,AT_MOST / WRAP_CONTENT 看做成雜牌,精確值看做成價格,View的大小看做價格。

  • 如果是衣服(子 View)產地是高檔品牌(LayoutParams = LayoutParams.MATCH_PARENT),商城是(父 View)高檔的商城(EXACTLY),那麼衣服的價格(size 大小)就會根據高檔商城價格來定,能有多高就賣多高(View的大小取決於父View大小)。

  • 如果是衣服(子 View)產地是高檔品牌(LayoutParams = LayoutParams.MATCH_PARENT),商城是(父 View)雜牌的商城(AT_MOST),那麼衣服的價格(size 大小)也會根據低檔商城價格來定,太高普通人也買不起呀(View的大小取決於父View大小)。

  • 如果是衣服(子 View)產地是雜牌(LayoutParams = LayoutParams.WRAP_CONTENT),商城是(父 View)高檔的商城(EXACTLY),那麼衣服的價格(size 大小)也會根據高檔商城價格來定,能有多高就賣多高,畢竟店大欺人,絕不打折(View的大小取決於父View大小)。

  • 如果是衣服(子 View)產地是雜牌(LayoutParams = LayoutParams.WRAP_CONTENT),商城是(父 View)雜牌的商城(AT_MOST),那麼衣服的價格(size 大小)就會根據低檔商城價格來定,小巷步行街不都是這樣賣的嗎(View的大小取決於父View大小)

  • 如果是衣服(子 View)已經全國明碼標價(android:layout_xxxx=”200dp”),商城是(父 View)無論是雜牌的商城(AT_MOST)還是高檔的商城(EXACTLY),那麼衣服的價格(size 大小)就不會變的。,不然打你小屁屁。

如果你覺得例子真的糟糕透了,那麼看以下一表正經總結:

一表正經總結以下:

  • 當父View的mode是EXACTLY的時候:說明父View的大小是確定的

    • 子View的寬或高是MATCH_PARENT:
    • 子View的寬或高是WRAP_CONTENT:子View是包裹布局,說明子View的大小還不確定,所以子View最大不能超過父View的大小mode=AT_MOST。
    • 子View的寬或高是具體數值:子viewd大小已經固定了,子View的大小就是固定這個數值,mode=EXACTLY。
  • 當父View的mode是AT_MOST的時候:說明父View大小是不確定的。

    • 子View的寬或高是MATCH_PARENT:父View大小是不確定的,子View是填充佈局情況,也不能確定大小,所以View大小不能超過父View的大小,mode=AT_MOST
    • 子View的寬或高是WRAP_CONTENT:子View是包裹布局,大小不能超過父View的大小,mode=AT_MOST。
    • 子View的寬或高是具體數值:子viewd大小已經固定了,子View的大小就是固定這個數值,mode=EXACTLY。
      需要注意一點就是,此時的MeasureSpec並不是View真正的大小,只有setMeasuredDimension之後才能真正確定View的大小。

measure

measure 主要功能就是測量設定 View 的大小。該方法是 final 型別,子類不能覆蓋,在方法裡面會呼叫 onMeasure(),我們可以複寫 onMeasure() 方法去測量設定 View 的大小。

  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
                 /*-----------省略程式碼---------------*

               onMeasure(widthMeasureSpec, heightMeasureSpec);

                  /*-----------省略程式碼---------------*/
    }

在 onMeasure( ) 方法中

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

onMeasure( ) 方法就是執行測量設定 View 程式碼的核心所在。

我們先來看下 getSuggestedMinimumWidth()

   protected int getSuggestedMinimumWidth() {
        //返回建議 View 設定最小值寬度
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

這裡返回的建議最小值就是我們xml 佈局中用的屬性 minWidth或者是背景大小。

同理可得 getSuggestedMinimumHeight()

看下 getDefaultSize

主要作用就是根據View的建議最小值,結合父View傳遞的measureSpec,得出並返回measureSpec

看程式碼

        public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //獲取父View傳遞過來的模式
        int specMode = MeasureSpec.getMode(measureSpec);
        //獲取父View傳遞過來的大小
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;//View的大小父View未定,設定為建議最小值 
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

getDefaultSize 的邏輯跟我們之前分析的 MeasureSpec 轉化規則非常相似。就是根據specMode設定大小。如果specMode是UNSPECIFIED 未確定大小,則會使用建議最小值,如果其他兩種情況,則使用父View傳遞過來的大小。再次強調:並不是父View 獨自決定,它是根據父 view 的MeasureSpec加上子vIew的自己的LayoutParams,通過相應的規則轉化而得到的大小。

再來看下 setMeasuredDimension

setMeasuredDimension 作用就是將測量好的寬跟高進行儲存。在onMeasure() 必須呼叫這個方法,不然就會丟擲 IllegalStateException 異常。

我們重新梳理一下剛才那些流程:
在measure 方法,核心就是呼叫onMeasure( ) 進行View的測量。在onMeasure( )裡面,獲取到最小建議值,如果父類傳遞過來的模式是MeasureSpec.UNSPECIFIED,也就是父View大小未定的情況下,使用最小建議值,如果是AT_MOST或者EXACTLY模式,則設定父類傳遞過來的大小。
然後呼叫setMeasuredDimension 方法進行儲存大小。

layout()

作用描述
measure() 方法中我們已經測量出View的大小,根據這些大小,我們接下來就需要確定 View 在父 View 的位置進行排版佈局,這就是layout 作用。
對 View 進行排版佈局,還是要看父 View,也就是 ViewGroup。

看程式碼

    @Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        super.layout(l, t, r, b);
    } else {
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}

程式碼不多,大致作用就是判斷 View 是否在執行動畫,如果是在執行動畫,則等待動畫執行完呼叫 requestLayout(),如果沒有新增動畫或者動畫已經執行完了,則呼叫 layout(),也就是呼叫View的 layout()。

public void layout(int l, int t, int r, int b) {

     /*-----------省略程式碼---------------*/
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);

        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

   /*-----------省略程式碼---------------*/
}

View 的 layout 的方法也是非常長。大致作用就是設定 View 的在父 View 的位置,然後判斷位置是否發生變化,是否需要重新呼叫排版佈局,如果是需要重新佈局則用了 onLayout()方法。
在OnLayout 方法中,View 裡面是一個空實現,而 ViewGroup 則是一個抽象方法。為什麼這麼設計呢?因為onLayout中主要就是為了給遍歷View然後進行排版佈局,分別設定View在父View中的位置。既然如此,那麼View的意義就不大了,而ViewGruo 必須實現,不然沒法對子View進行佈局。那麼如何對 View 進行排版呢?舉例個簡單的demo

protected void onLayout(boolean changed,
                        int l, int t, int r, int b) {

    int childCount = getChildCount();
    for (
            int i = 0;
            i < childCount; i++)

    {
        View child = getChildAt(i);
        child.layout(l, t, r, b);
    }
}

就是遍歷所有的子 View 然後呼叫 child.layout(l, t, r, b)。

大家有興趣也可以參考一下 FrameLayout, LinearLayout這類佈局。

draw()

經過前面兩部的測量跟佈局之後,接下來就是繪製了,也就是真正把 View 繪製在螢幕可見檢視上。draw()作用就是繪製View 的背景,內容,繪製子View,還有前景跟滾動條。看下 View 的draw() 原始碼

@CallSuper
public void draw(Canvas canvas) {

    /*-----------省略程式碼---------------*/
    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

  /*-----------省略程式碼---------------*/
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

     /*-----------省略程式碼---------------*/
        return;
    }

draw 過程中一共分成7步,其中兩步我們直接直接跳過不分析了。

第一步:drawBackground(canvas): 作用就是繪製 View 的背景。

第三步:onDraw(canvas) :繪製 View 的內容。View 的內容是根據自己需求自己繪製的,所以方法是一個空方法,View的繼承類自己複寫實現繪製內容。

第三步:dispatchDraw(canvas):遍歷子View進行繪製內容。在 View 裡面是一個空實現,ViewGroup 裡面才會有實現。在自定義 ViewGroup 一般不用複寫這個方法,因為它在裡面的實現幫我們實現了子 View 的繪製過程,基本滿足需求。

第四步:onDrawForeground(canvas):對前景色跟滾動條進行繪製。

第五步:drawDefaultFocusHighlight(canvas):繪製預設焦點高亮

draw 流程圖

好了,整個繪製流程就分析完畢了!