天天看點

View & ViewGroup 繪制原理概要View 繪制ViewGroup 繪制參考文章

概要

Activity

界面的根布局其實是一個名為

DecorView

FrameLayout

, 在建立

DecorView

的時候,會相應的建立一個

ViewRootImpl

類來控制 UI 的繪制,其實也就是控制

DecorView

。繪制的過程分為了

measure

,

layout

,

draw

三個過程。 以下分别對

View

ViewGroup

三個過程進行說明。

View 繪制

measure

public final void measure(int widthMeasureSpec, int heightMeasureSpec)
           

兩個參數

widthMeasureSpec

heightMeasureSpec

parent

提供過來的,

parent

用這兩個參數告訴

View

,我希望你的寬高是多大。這兩個 參數可以由

View

中的一個靜态類

MeasureSpec

來解析,

MeasureSpec

是由

size

mode

組成,可由下面方法獲得。

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
           

size

很好了解 ,就是

parent

Child View

的大小。 而

mode

呢,有三種形式。

  1. UNSPECIFIED

    parent

    Child View

    沒有任何限制,

    Child View

    想多大就多大。
  2. EXACTLY

    parent

    child view

    說,我隻希望是這個固定的大小,也就是上面代碼解析出來的

    specSize

  3. AT_MOST

    parent

    告訴

    child view

    ,你最大繪制範圍就是

    specSize

那麼

View.onMeasure()

就是利用

paren

t 傳過來的關于寬高限制的參數來決定自己的寬度 ,然而可以看到這個方法是

final

,不允許重寫的。 當然,實際的測量是在

View.onMeasure()

方法中。

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

系統調用了

setMeasuredDimension()

,這是測量之後必須要調用的方法,它用來存儲的測量的寬高,也就是

getMeasuredWidth()

getMeasuredHeight()

的值。

系統的實作中還用到了

getSuggestedMinimumWidth()

getSuggestedMinimumHeight()

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

為何要調用這個?有時候,我可能設定了

android:backgound

或者

android:minWidth

,

android:minHeight

,是以我們自定義的

View

不應該小于這個值吧。

當然我們還想知道,系統的預設實作中設定的測量寬高是什麼呢,這個是要看

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

如果解析出來的

mode

UNSPECIFIED

,也就說

parent

Child View

沒有限制,那就用

getSuggestedMinimumXXX()

的值。

如果解析出來的

mode

AT_MOST

EXACTLY

, 就用

parent

提供過來的大小。 那也就是說,如果調用系統的方法

super.onMeasure()

進行測試,預設就是填充整個父布局。 我們自定義

View

ViewGroup

的時候,有時候并不想這樣,是以需要自己測量,最後調用

setMeasuredDimension()

方法。

draw

@CallSuper
    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
        int saveCount;

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

        // 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 (foreground, scrollbars)
        onDrawForeground(canvas);
    }
           

View.draw()

方法并不是

final

的,是以我們可以重寫它,不過一定記得調用

super.draw(canvas)

, 因為系統幫我們繪制了

background

scrollBar

,而且最重要的是,如果是

ViewGroup

的話,會調用

dispatchDraw()

來讓

Child View

進行繪制。這也正是

@CallSuper

注解的意思。

從代碼中的

step 3

可以看出,

onDraw()

才是繪制自己的内容區域。而

View

onDraw()

方法是一個空方法

/**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }
           

layout

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != ) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
    }
    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);
    }
}
           

View

layout()

parent

(

ViewGroup

) 調用,用來确定

Child View

的位置,順便也确定了實際大小。如果是自定義 View 的話,完全沒必要在意這個方法,當然也不用在意在

View.layout()

調用的

View.onLayout()

方法。而在實際中,隻需要注意

onSizeChange()

方法,這個方法告訴你,你的

View

的大小發生了改變。而

onSizeChange()

就是在

setOpticalFrame()

或者

setFrame()

中調用的,而這個方法是

layout

過程的關鍵所在 ,它們決定了

View

的實際寬高,我們取其中一個方法看看樣子

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;

        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        // 檢測大小是否已經改變
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // Invalidate our old position
        invalidate(sizeChanged);
        // 更新坐标值
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        if (sizeChanged) {
            // 調用了 onSizeChanged()
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
    }
    return changed;
}
           

setFrame()

檢測了

View

的大小是否改變了,然後更新了坐标值。 坐标值的改變也意味着

getWidth()

getHeight()

的值可以确定了。

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

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

如果大小改變了,就調用

sizeChange()

方法

private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
        // ...
    }
           

sizeChange()

方法中首先就調用了

onSizeChanged()

方法。

那麼我們梳理下

layout

過程,

layout()

->

onSizeChanged()

->

onLayout()

ViewGroup 繪制

measure

View

measure()

方法為

final

類型,是以子類不能複寫這個方法,隻能複寫

onMeasure()

方法,那麼

ViewGroup

作為

View

的子類,也不例外。

我們還可以知道,

onMeasure()

是要完成自身的測量,并通過調用

setMeasuredDimension()

方法來存儲測量的結果。 然而對于

ViewGroup

來說,它需要提供一個

MeasureSpec

給它的每個

Child View

用來完成測量,然後根據

Child View

測量結果來決定自身的測量值。

ViewGroup

用來測量

Child View

的方法有三個,分别是

measureChild()

,

measureChildren()

,

measureChildWithMargins()

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


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

        int size = Math.max(, specSize - padding);

        int resultSize = ;
        int resultMode = ;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= ) {
                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 >= ) {
                // 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 >= ) {
                // 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 ?  : 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 ?  : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
           

首先整體觀看

measureChild()

方法,它隻考慮了

padding

,而并沒有考慮經常會出現的

margin

,是以,如果自定義

View

的時候沒有

margin

可以考慮用這個方法完成

Child View

的測量。

measureChild()

是調用

getChildMeasureSpec()

來為

Child View

生成相應的

MeasureSpec

,它的第二個參數雖然名為

padding

,但是在實際使用的時候,它其實在更嚴格的意義上講,是指已經使用的長度或寬度。

mesureChildren()

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = ; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
           

measureChildren()

在原理還是調用了

measureChild()

方法,隻是有一點需要注意,如果

Child View

的可見性為

GONE

,是不會測量的。

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

measureChildWithMargins()

這個方法就比較全面,首先考慮了

padding

margin

,其次它需要傳入兩個參數,分為代表已經使用過的寬度和高度,這樣我們就很友善計算并排的

Child View

的寬高。

FrameLayout.onMeasure()

說了這麼多很空泛,舉個例子。 我們知道,根布局其實就是一個名為

DecorView

FrameLayout

,它的

onMeasure()

方法的部分源碼如下

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = ;
        int maxWidth = ;
        int childState = ;

        for (int i = ; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, , heightMeasureSpec, );
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                //...
            }
        }

        // ...

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        // ...
    }
           

在第

17

行,它使用的是

measureChildWithMargins()

方法來完成

Child View

的測量,由于

FrameLayout

設計為層層覆寫的特性,是以

measureChildWithMargins()

的第三個參數和第五個參數都為0,代表永遠沒有使用過的寬度來高度,隻考慮

padding

margin

即可。

layout

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

    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);
           

ViewGroup

layout()

方法其實也是調用

View

layout()

完成布局的。 然而我們注意到

ViewGroup

onLayout

是一個抽象方法,子類必須實作,這也是提示我們,自定義

ViewGroup

就有責任為

Child View

布局。

draw

ViewGroup

如果自身沒有特别的繪制需要,就不需要複寫

draw()

或者

onDraw()

方法,它的重點在布局。

draw()

方法會繪制一些基本的東西,例如背景,滾動條等等,是以如果想複寫

draw()

方法記得調用

super.draw()

,而如果複寫

onDraw()

隻需要完成自己的繪制即可 。

參考文章

https://developer.android.google.cn/training/custom-views/create-view.html

https://realm.io/news/360andev-huyen-tue-dao-measure-layout-draw-repeat-custom-views-and-viewgroups-android/

http://blog.csdn.net/yanbober/article/details/46128379

繼續閱讀