天天看點

Android View的工作原理(二)之 View的工作流程View的工作流程

View的工作流程

前面《Android View的工作原理(一)之 View的三大過程和 認識MeasureSpec》中我們介紹了View出現在螢幕要經過三個過程:measure、layout 和 draw和它們各自的作用,以及MeasureSpec的獲得情況。今天将沿着前面内容繼續探讨View的工作原理,其中大家就會明白MeasureSpec的用途和三大過程的工作流程。

measure過程

measure就是測量,在某些極端情況下,系統可能需多次measure才能确定最終的測量寬/高,在這情形下,在onMeasure方法中拿到的測量寬/高很可能是不準确的。一個比較好的習慣是在onLayout方法中去擷取View的測量寬/高或最終寬/高。

View的measure過程

View是通過measure方法就完成了其測量過程。View的measure是一個 final方法。在measure方法中會去調用onMeasure方法,onMeasure方法如下所示:

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

setMeasuredDimension方法會設定View寬/高的測量值,再來看看getDefaultSize方法代碼:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = View.MeasureSpec.getMode(measureSpec);
    int specSize = View.MeasureSpec.getSize(measureSpec);
    switch(specMode) {
    case MeasureSpec.EXACTLY:
    case MeasureSpec.AT_MOST:
        result = specSize;
        break;
    case MeasureSpec.UNSPECIFIED:
        result = size;
	break;
    }
    return result;
}
           

在AT_MOST和EXACTLY兩種情況下,傳回的大小其實就是前面提到的MeasureSpec中的SpecSize,而這個specSize就是View測量後的大小(最終大小是在layout階段确定)。

在UNSPECIFIED情況下,一般用于系統内部的測量過程,傳回大小為第一個參數size,即getSuggestedMinimumWidth和getSuggestedMinimumHeight兩個方法傳回值,源碼如下:

protected int getSuggestedMinimumWidth() {
    int suggestedMinWidth = this.mMinWidth;
    if(this.mBGDrawable != null) {
        int bgMinWidth = this.mBGDrawable.getMinimumWidth();
        if(suggestedMinWidth < bgMinWidth) {
            suggestedMinWidth = bgMinWidth;
        }
    }

    return suggestedMinWidth;
}
           

getSuggestedMinimumWidth的邏輯:如果View沒有設定背景,那麼傳回mMinWidth的值(android:minWidth屬性的值,不設定則為0);如果設定了背景,則傳回mMinWidth和背景的最小寬度兩者中最大值。 

ViewGroup的measure過程

ViewGroup除了要完成自己的measure過程外,還要周遊所有子元素的measure方法讓所有的子元素都執行measure過程。和View不同的是,ViewGroup是一個抽象類,是以它沒有重寫View的onMeasure方法,但它提供了一個叫measureChildren的方法,如下所示:

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);
        }
    }
}
           

ViewGroup在measure時,會對每一個子元素進行measure,measureChild這個方法如下所示:

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);
}
           

LinearLayout的onMeasure方法分析

ViewGroup并沒有定義其measure的具體過程,這是因為它是一個抽象類,其measure過程的onMeasure方法需要各個子類去具體實作,比如LinearLayout、RelativeLayout等,我們來看看LinearLayout的onMeasure方法,如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}
           

可以看到LinearLayout中垂直和水準方式分别由measureVertical和measureHorizontal兩個方法來具體實作。

layout過程

layout就是布局,layout的作用是ViewGroup用來确定子元素的位置,當ViewGroup的位置被确定後,它在onLayout中會周遊所有的子元素并調用期layout方法,在layout方法中onLayout方法會被調用。來看View的layout方法,如下所示:

@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    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);
        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);
            }
        }
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
           

首先會通過setFrame方法來設定View的四個頂點位置,即初始化mLeft、mTop、mBottom和mRight;

接下來調用onLayout方法來确定子元素的位置,onLayout的具體實作同樣和具體的布局有關,是以View和ViewGroup均沒有真正實作onLayout方法,都交由繼續它們的子類各自實作。

draw過程

draw就是最後的繪制了,draw過程比較簡單,它的作用是将View繪制到螢幕上面。繪制過程遵循如上幾步:

1、  繪制背景

2、  繪制自己(onDraw方法)

3、  繪制子元素(dispatchDraw方法)

4、  繪制裝飾(onDrawScrollBars方法)

draw的源碼這裡略過,我們來看看View有一個特殊的方法setWillNotDraw,先看一下它的源碼:

public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
           

如果一個View不需要繪制任何内容,那麼會設定一個标記為true,系統會進行相應的優化。預設情況下,ViewGroup就是啟用了這個優化标記,是以ViewGroup預設是不進行繪制功能的。當我們繼承ViewGroup自定義一個ViewGroup控件而且需要ViewGroup本身進行繪制的情況下,那麼就需要使用setWillNotDraw方法傳遞false。

——本博文部分内容參考自《Android開發藝術探索》

繼續閱讀