【Android】View的繪製原理

1、View繪製總入口

ActivityThread中,首先建立Activity,而後經過attach方法初始化對應的mWindow,而後將頂級視圖DecorView添加到Windows中,並建立ViewRootImpl對象,這個對象就是溝通WindowManager和DecorView之間的橋樑,也是View繪製的開始。android

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

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

measure過程獲得的測量寬高能夠經過getMeasureWidth和getMeasureHeight獲得,其值不必定就是實際寬高,實際寬高是layout以後,能夠經過getWidth和getHeight得到。函數

2、measure

一、MeasureSpec

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

MeasureSpec提供打包和解包的方法,能夠將一組SpecMode和SpecSize經過makeMeasureSpec方法打包成MeasureSpec,也能夠將一個MeasureSpec經過getMode和getSize進行解包得到對應的值。post

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

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的計算。code

(1)DecorView的MeasureSpec值計算,DecorView是最早被測量的,能夠從ViewRootImpl的performMeasure方法看出。orm

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就是父容器可用剩餘空間。

二、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。

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

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

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

3、draw

draw的過程就是將View繪製在屏幕上面。從Android源碼的註釋也能夠看見繪製分爲四個步驟,在View的draw方法中。
(1)繪製背景

drawBackground(canvas)

(2)繪製本身的內容

onDraw(canvas)

(3)繪製children

dispatchDraw(canvas);

(4)繪製裝飾

onDrawForeground(canvas);

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

相關文章
相關標籤/搜索