1. 程式人生 > >【Android】View的繪製原理

【Android】View的繪製原理

一、View繪製總入口

ActivityThread中,首先建立Activity,然後通過attach方法初始化對應的mWindow,然後將頂級檢視DecorView新增到Windows中,並建立ViewRootImpl物件,這個物件就是溝通WindowManager和DecorView之間的橋樑,也是View繪製的開始。

View的繪製流程首先開始於ViewRootImpl的performTraversals()方法。依次經過三大過程,measure、layout、draw,performTraversals會依次呼叫performMeasure、performLayout、performDraw方法。其中measure用來對View進行測量,layout來確定子元素在父元素中的位置即真實寬高以及四個頂點位置,draw負責將View繪製出來。

說的接地氣一點,measure就是給個建議值,View多大合適;layout就是去放View的外框,放在哪裡,具體多大;draw就是去畫View裡面的內容。

measure過程得到的測量寬高可以通過getMeasureWidth和getMeasureHeight得到,其值不一定就是實際寬高,實際寬高是layout之後,可以通過getWidth和getHeight獲得。

二、measure

1、MeasureSpec

測量過程需要提到一個類,叫MeasureSpec,“測量規格”。MeasureSpec是View定義的一個內部類。MeasureSpec代表一個32位的int,高兩位代表SpecMode,測量模式,第30位代表SpecSize,在某種測量模式下的規格大小。

MeasureSpec提供打包和解包的方法,可以將一組SpecMode和SpecSize通過makeMeasureSpec方法打包成MeasureSpec,也可以將一個MeasureSpec通過getMode和getSize進行解包獲得對應的值。

SpecMode測量模式包含三種,含義如下:

SpecMode值 含義
UNSPECIFIED 父容器沒有對View有任何限制,要多大給多大
EXACTLY 父容器能得到View的精確的值,這時候View的測量大小就是SpecSize的值,對應於View的LayoutParams中為match_parent或具體的值的情況。
AT_MOST 父容器指定了一個可用大小SpecSize,View的大小不定,但是不能大於這個值,這個對應於View的LayoutParams的wrap_content。

系統需要通過MeasureSpec對View進行測量。View的MeasureSpec需要由父容器的MeasureSpec和View的View的layoutParams一起決定,然後根據View的MeasureSpec確定View的寬和高。

由於DecorView是頂級檢視,所以它的測量方法比較特殊,具體下面一一看下DecorView和普通View的MeasureSpec的計算。

(1)DecorView的MeasureSpec值計算,DecorView是最先被測量的,可以從ViewRootImpl的performMeasure方法看出。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

這裡的mView就是DecorView。往前搜傳入的兩個MeasureSpec的值。

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

可以看見呼叫了getRootMeasureSpec方法,傳入了兩個值,第一個值是螢幕尺寸,第二個值lp是LayoutParams 長寬的引數。所以,DecorView的MeasureSpec的值是由螢幕尺寸和它的LayoutParams 決定的。接下來看具體的關係getRootMeasureSpec。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

可以看見如果是MATCH_PARENT的話,就是精確的模式,大小就是視窗的大小,如果是WRAP_CONTENT,就是AT_MOST模式,即大小不定,但是最大為視窗大小。
(2)普通View的MeasureSpec值計算。
前面說到DecorView首先被測量,呼叫了mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),由於DecorView繼承自FrameLayout繼承自ViewGroup繼承自View,可以看見View裡面的measure方法是一個final方法,即不可被重寫。

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

這個方法其實是去呼叫了onMeasure方法,這個方法是各個子類可以重寫的。

跟蹤這個方法,可以看見在View的measure是由ViewGroup傳遞過來的,具體是在ViewGroup裡面的measureChildWithMargins方法。

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

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

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看見,首先獲取View的LayoutParams,然後呼叫getChildMeasureSpec方法獲取View的MeasureSpec,再呼叫View的measure進行測量,測量後面說,先說View的MeasureSpec的計算。看getChildMeasureSpec方法。

先看傳入的三個引數:

  • 第一個引數parentWidthMeasureSpec即父容器的MeasureSpec
  • 第二個引數mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed,這些都是啥呢?這些分別是父容器設定的padding和View設定的margin和父容器已經被佔用的大小。所以這些值加起來就是父容器中不能被View使用的長度。可以看見原始碼中函式引數名字就是padding,雖然不很確切,但是也可以說明就是不能被View使用的部分。
    第三個引數是View的LayoutParams。確切地說就是XML中layout_width和layout_height。

看下具體的getChildMeasureSpec方法。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    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:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } 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't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } 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't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    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);
}

一句句看下來,首先,獲取父容器的specMode和specSize,然後計算size = Math.max(0, specSize - padding),這裡size就是說是父容器留給View的最大長度,如果specSize < padding,說明沒有空間給View了,所以是0,否則能給View的剩餘空間就是specSize - padding,這個比較好理解。

然後判斷父容器specMode。根據父容器的specMode和View的LayoutParams值(match_parent、具體值、wrap_content)來決定。具體的邏輯非常簡單,程式碼容易看懂。

總結成如下表:

View的LayoutParams\父容器的測量模式specMode EXACTLY AT_MOST UNSPECIFIED
具體的值 EXACTLY childsize EXACTLY childsize EXACTLY childsize
match_parent EXACTLY parentsize AT_MOST parentsize UNSPECIFIED 0
wrap_content AT_MOST parentsize AT_MOST parentsize UNSPECIFIED 0

這裡的parentsize就是父容器可用剩餘空間。

2、View的measure過程

首先,View的measure過程是由measure方法完成,看下View的measure方法。可以看見是個final方法,即不可被重寫。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) 

measure方法會呼叫onMeasure方法,看onMeasure方法的實現。

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

setMeasuredDimension即設定View的測量寬高的值。計算方法是呼叫getDefaultSize,傳入兩個引數,先看這兩個引數。
第二個引數是MeasureSpec,即前面說的,通過父MeasureSpec和View的LayoutParams以及父容器已經佔用的空間進行計算得到。
看下第一個引數getSuggestedMinimumWidth()。

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

思路比較簡單,如果沒有背景,則直接返回mMinWidth,mMinWidth即View的android:minWidth的設定值,預設為0;如果有背景,則返回max(mMinWidth, mBackground.getMinimumWidth(),mBackground.getMinimumWidth()為背景的尺寸,預設也是0。
現在看下getDefaultSize方法。

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

思路也是比較簡單的,如果測量模式是UNSPECIFIED,返回第一個引數,如果是AT_MOST和EXACTLY,直接返回View的MeasureSpec的規格大小。

從上面的分析可以發現,直接繼承View的時候,需要重寫onMeasure方法,並設定wrap_content時候的大小,否則會和match_parent的時候效果一直。比如當View設定為wrap_content的時候,此時View的MeasureSpec為AT_MOST,parentsize。當View設定為match_parent的時候,其測量值最後的結果也為parentsize。

3、ViewGroup的measure過程

ViewGroup除了完成自己的measure過程外,還會去遍歷所有的子元素的measure方法,各個子元素再去遞迴這個過程。ViewGroup是個抽象類,其onMeasure方法是個抽象方法,需要子類去實現它,因為不同的ViewGroup有不一樣的佈局特性,所以導致他們的測量細節不一樣。

ViewGroup提供一個遍歷的方法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);
        }
    }
}

遍歷所有的View,如果可見的話呼叫measureChild進行測量。getChildMeasureSpec的實現前面已經講過了。得到之後呼叫子元素的measure方法。

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

4、measure結果的獲取

有的業務需要,需要獲取測量結果,但是有個問題,measure的測量和Activity的各個生命週期沒有關係,所以在什麼生命週期裡面都有可能得到的measure測量值為0,即還沒測量結束。從原始碼可以看見performTraversals是另開一個執行緒執行的。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

如果保證能獲取到呢?有幾個方法:
(1)為View新增onWindowFocusChanged監聽,這個監聽會在每次Activity的視窗獲得和失去焦點的時候被呼叫,而View繪製成功的時候也是一次獲得焦點的過程,所以也會呼叫。

public void onWindowFocusChanged(boolean hasWindowFocus) {
    super.onWindowFocusChanged(hasWindowFocus);
    if (hasWindowFocus) {
        int height = view.getMeasureHeight();
        int width = view.getMeasureWidth();
    }
}

(2)view.post
通過post將一個runnable投遞到訊息佇列尾部,然後呼叫的時候,說明View已經初始化好了。

view.post(new Runnable() {
    @Override
    public void run() {
        int height = view.getMeasureHeight();
        int width = view.getMeasureWidth();
    }
});

(3)view.measure(int widthMeasureSpec, int heightMeasureSpec)
這個是最直接的方法,直接觸發計算。這個一般是用在具體的值上,因為parentsize不知道。

比如寬高都是100px的。

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

二、layout

layout的作用是ViewGroup用來確定子元素的位置。首先呼叫setFrame初始化View的四個頂點,接著呼叫onLayout方法。

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

onLayout由於不同的子類實現細節都不一樣,所以onLayout在View和ViewGroup中都沒有具體實現,都在子類中實現。以LinearLayout為例。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

其中layoutVertical關鍵程式碼如下。

for (int i = 0; i < count; i++) {
    final View child = getVirtualChildAt(i);
    if (child == null) {
        childTop += measureNullChild(i);
    } else if (child.getVisibility() != GONE) {
        final int childWidth = child.getMeasuredWidth();
        final int childHeight = child.getMeasuredHeight();
        
        final LinearLayout.LayoutParams lp =
                (LinearLayout.LayoutParams) child.getLayoutParams();
        
        int gravity = lp.gravity;
        if (gravity < 0) {
            gravity = minorGravity;
        }
        final int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                        + lp.leftMargin - lp.rightMargin;
                break;

            case Gravity.RIGHT:
                childLeft = childRight - childWidth - lp.rightMargin;
                break;

            case Gravity.LEFT:
            default:
                childLeft = paddingLeft + lp.leftMargin;
                break;
        }

        if (hasDividerBeforeChildAt(i)) {
            childTop += mDividerHeight;
        }

        childTop += lp.topMargin;
        setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                childWidth, childHeight);
        childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

        i += getChildrenSkipCount(child, i);
    }
}

可以看見是一個逐漸往下的過程。在父容器完成定位後,呼叫setChildFrame呼叫子元素的layout。

private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height);
}

這裡的width和height就是測量寬高。

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

看下最後真正的寬高的計算方法如下:

public final int getWidth() {
    return mRight - mLeft;
}
public final int getHeight() {
    return mBottom - mTop;
}

顯然這個地方得到的值就是width和height。所以說在View的預設實現中,View的measure的結果測量寬高和layout的結果最終寬高是相等的。除非重新View,使得兩者不一致。比如下面這樣,但是這個沒有什麼意義。

public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}

三、draw

draw的過程就是將View繪製在螢幕上面。從Android原始碼的註釋也可以看見繪製分為四個步驟,在View的draw方法中。
(1)繪製背景

drawBackground(canvas)

(2)繪製自己的內容

onDraw(canvas)

(3)繪製children

dispatchDraw(canvas);

(4)繪製裝飾

onDrawForeground(canvas);

其中View的繪製通過dispatchDraw來遍歷子元素並呼叫其draw方法,將draw事件一層層傳遞下去。