Android開發藝術(3)——View的工做原理

View的工做流程

View的工做流程,就是measure、layout和draw。measure用來測量View的寬高,layout用來肯定View的位置,draw則用來繪製View。這裏measure較爲複雜主要分析一下,measure流程分爲View的measure流程和ViewGroup的measure流程,只不過ViewGroup的measure流程除了要完成本身的測量還要遍歷去調用子元素的measure()方法。java

View的測量

先來看看onMeasure()方法(View.java):android

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

在這以前還有個measure()方法直接調用的上面的onMeasure()方法,這裏measure()唄final修飾因此沒法從新因此主要看看onMeasure()裏的setMeasuredDimension()方法:canvas

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
       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;
       }
       setMeasuredDimensionRaw(measuredWidth, measuredHeight);
   }複製代碼

大概意思是用來設置View的寬高的,接下來在看看getDefaultSize()方法處理了什麼:bash

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;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}複製代碼

specMode是View的測量模式,而specSize是View的測量大小,看到這裏咱們有必要先說說MeasureSpec:
MeasureSpec類幫助咱們來測量View,它是一個32位的int值,高兩位爲specMode (測量的模式),低30位爲specSize (測量的大小),測量模式分爲三種:微信

  • UNSPECIFIED:未指定模式,View想多大就多大,父容器不作限制,通常用於系統內部的測量。app

  • AT_MOST:最大模式,對應於wrap_comtent屬性,只要尺寸不超過父控件容許的最大尺寸就行。ide

  • EXACTLY:精確模式,對應於match_parent屬性和具體的數值,父容器測量出View所須要的大小,也就是specSize的值。佈局

讓咱們回頭看看getDefaultSize()方法,很顯然在AT_MOST和EXACTLY模式下,都返回specSize這個值,也就是View測量後的大小,而在UNSPECIFIED模式返回的是getDefaultSize()方法的第一次個參數的值,這第一個參數從onMeasure()方法來看是getSuggestedMinimumWidth()方法和getSuggestedMinimumHeight()獲得的,那咱們來看看getSuggestedMinimumWidth()方法作了什麼,咱們只須要弄懂getSuggestedMinimumWidth()方法,由於這兩個方法原理是同樣的:post

protected int getSuggestedMinimumWidth() {
      return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
  }複製代碼

很明瞭,若是View沒有設置背景則取值爲mMinWidth,若是View設置了背景在取值爲max(mMinWidth,mBackground.getMinimumWidth()),取值mMinWidth和mBackground.getMinimumWidth()的最大值,mMinWidth是能夠設置的,它對應於android:minWidth這個屬性設置的值或者View的setMinimumWidth的值,若是不指定的話則默認爲0,mBackground.getMinimumWidth(),這個mBackground是Drawable類型的,看一下Drawable類的getMinimumWidth()方法(Drawable.java):優化

public int getMinimumWidth() {
       final int intrinsicWidth = getIntrinsicWidth();
       return intrinsicWidth > 0 ? intrinsicWidth : 0;
   }複製代碼

intrinsicWidth獲得的是這個Drawable的固有的寬度,若是固有寬度大於0則返回固有寬度,不然返回0。
綜上:getSuggestedMinimumWidth()方法就是:若是View沒有設置背景則返回mMinWidth ,若是設置了背景就返回mMinWidth 和Drawable最小寬度兩個值的最大值。

ViewGroup的測量

講完了View的measure流程,接下來看看ViewGroup的measure流程,對於ViewGroup,它不僅要measure本身自己,還要遍歷的調用子元素的measure()方法,ViewGroup中沒有定義onMeasure()方,但他定義了measureChildren()方法,在咱們本身實現onMeasure時能夠調用它,也能夠不調用(通常測量孩子都調用他),至關於一個模板。在線性佈局、相對佈局等中都有實現,稍後分析。(ViewGroup.java):

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
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,內部再讓孩子去measure,因而就到了View的測量。這裏getChildMeasureSpec()方法裏寫了什麼呢?點擊去看看:

//三個參數分別是
//1.父View的measurespec
//2.父View已經佔用的尺寸,也就是孩子不能使用的(這個是父View的padding+孩子的margin)
//3.子view的width(MATCH_PARENT、WARP_CONTENT、具體數值)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
          //父容器的可用尺寸(去掉了padding),若是是負的,那就是0
        int size = Math.max(0, specSize - padding);
        int resultSize = 0;
        int resultMode = 0;
        switch (specMode) {
        // Parent has imposed an exact size on us
        //父容器本身是精確的模式,也就是能夠肯定父容器的尺寸了(要嘛是具體數值,要嘛是父親的父親的寬度,反正是肯定的)
        case MeasureSpec.EXACTLY:
            //孩子的尺寸是具體數值(大於等於0就是具體數值)
            if (childDimension >= 0) {
                  //以下
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
              //孩子是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;

              //孩子是包裹內容
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can not be
                // bigger than us.
              //孩子是包裹內容,那麼孩子的測量模式就是AT_MOST,而且此時size的含義就是孩子最大可能的尺寸,而不是孩子的具體尺寸了
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent has imposed a maximum size on us
            //父親是AT_MOST,說明父親的尺寸不肯定,可是父親最大不能超過某個數值,這個數值是已知了
        case MeasureSpec.AT_MOST:
            //孩子是具體數值,那孩子就像下面那樣,精確模式、具體尺寸
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
              //孩子是MATCH_PARENT,那麼孩子不是精確的,可是孩子能夠肯定他最大尺寸,那就是父親的最大尺寸,模式是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;
              //孩子是包裹內容,那麼孩子的尺寸也是不能夠定的,可是最大值是知道的,不能超過父親的最大尺寸,以下。。
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can not be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            //這種狀況
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }複製代碼

很顯然這是根據父容器的MeasureSpec的模式再結合子元素的LayoutParams屬性來得出子元素的MeasureSpec屬性。

LinearLayout的measure流程

ViewGroup並無提供onMeasure()方法,而是讓其子類來各自實現測量的方法,究其緣由就是ViewGroup有不一樣的佈局的須要很難統一,接下來咱們來簡單分析一下ViewGroup的子類LinearLayout的measure流程,先來看看它的onMeasure()方法(LinearLayout.java):

@Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       if (mOrientation == VERTICAL) {
           measureVertical(widthMeasureSpec, heightMeasureSpec);
       } else {
           measureHorizontal(widthMeasureSpec, heightMeasureSpec);
       }
   }複製代碼

兩個方法實現大同小異,這裏看下垂直measureVertical()方法的部分源碼:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
     mTotalLength = 0;       
 ...
  for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }
            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
            totalWeight += lp.weight;

            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                // Optimization: don not bother measuring children who are going to use
                // leftover space. These views will get measured again down below if
                // there is any leftover space.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                int oldHeight = Integer.MIN_VALUE;
                if (lp.height == 0 && lp.weight > 0) {
                    // heightMode is either UNSPECIFIED or AT_MOST, and this
                    // child wanted to stretch to fill available space.
                    // Translate that to WRAP_CONTENT so that it does not end up
                    // with a height of 0
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
                       totalWeight == 0 ? mTotalLength : 0);
                if (oldHeight != Integer.MIN_VALUE) {
                   lp.height = oldHeight;
                }
                final int childHeight = child.getMeasuredHeight();
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
...
        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }
                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }
                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }
        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;
        // Check against our minimum height複製代碼

大體意思就是定義了mTotalLength用來存儲LinearLayout在垂直方向的高度,而後遍歷子元素,根據子元素的MeasureSpec模式分別計算每一個子元素的高度,若是是wrap_content則將每一個子元素的高度和margin垂直高度等值相加並賦值給mTotalLength得出整個LinearLayout的高度。若是佈局高度設置爲match_parent者具體數值則和View的測量方法同樣。

Layout

layout方法用來決定View自身的位置,在layout中調用了onLayout方法,這個方法沒有具體的實現,須要子類本身實現,主要是爲了決定子View的位置

public void layout(int l, int t, int r, int b) {
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
  //設置自身的位置
    boolean changed = setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        //調用onLayout,具體的實現都不同
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
        if (mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>) 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);
            }
        }
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
}複製代碼

在Linearlayout中,onLayout中主要就是遍歷孩子,而後調用setChildFrame方法,這個方法內部就是調用child的layout方法,因此又回到了上面那一步。

Draw

public void draw(Canvas canvas) {

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */
    // Step 1, draw the background, if needed

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        // we're done... return; } // Step 2, save the canvas' layers

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);
    // Step 4, draw the children
    dispatchDraw(canvas);
    // Step 5, draw the fade effect and restore layers

    // Step 6, draw decorations (scrollbars)
    onDrawScrollBars(canvas);
}複製代碼

看註釋可知,View的draw過程主要有如下幾步:

  • 畫背景

  • 畫內容

  • 畫孩子

  • 畫裝飾

draw是經過dispatchDraw將繪畫分發給孩子的

有個方法是setWillNotDraw(),能夠設置當前view不繪製內容,通常繼承自ViewGroup,而且確保自身不須要繪製,就設爲true,能夠優化。默認爲false。

自定義view

自定義view這塊就大概說一下注意事項,具體不展開。若是你想深刻了解這裏強烈推薦一下凱哥的自定義View系列 HenCoder:給高級 Android 工程師的進階手冊,若是還沒看過你就out了, 良心巨做,如今好像都開始着手準備國際化了推向國外了,凱哥(扔物線)的「關注我就能達到大師級水平,這話我終於敢說了」可不是蓋的。

  • 繼承自View的自定義View

    在onMeasure中處理wrap_parent
    在onDraw中處理padding
    自定義xml屬性,文件名字不必定要交attrs。自定義屬性獲取完數據以後記得調用recycle。繼承自ViewGroup的自定義View

  • 在onMeasure中調用measureChildren測量孩子(也能夠本身寫邏輯),而後分析本身的measurespec,最後調用setMeasuredDimension
    onLayout中根據測量寬高,遍歷孩子,爲其佈局。

這裏最後放一張HenCoder:給高級 Android 工程師的進階手冊的微信公衆號的圖片,感謝大神的無私奉獻~~~

相關文章
相關標籤/搜索