天天看點

view的onMeasure,onLayout,onDraw源碼分析

1, View三部曲

在oncreate方法中加載解析完xml資源建立view對象之後,Activity中的makeVisible方法會将這些對象依次測量,确定位置并且顯示在幕布上。總體的流程圖如下,

view的onMeasure,onLayout,onDraw源碼分析

重點分析onMeasure,onLayout,和onDraw方法。

1.1 onMeasure

measure是測量的意思,那麼onMeasure()方法顧名思義就是用于測量視圖的大小的.

1.1.1 view

首先看沒有子view的測量方法.

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
           
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;
    }
           

MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小(後30位),specMode記錄的是規格(前2位). specMode一共有三種類型。

變量widthMeasureSpec和heightMeasureSpec都是ViewRoot的performTraversals方法中擷取的,

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
           
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;
    }
           

根視圖總是會充滿全屏的。

1.1.2 ViewGroup

既然子view都實作了onMeasure方法,那麼是不是也有onMeasure方法,并且逐個周遊目前布局下的所有子view呢?

   檢視ViewGroup源碼,發現并沒有實作onMeasure方法,但是有一個protected類型的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);
            }
        }
    }
           
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);
    }
           

看來真的會周遊目前布局下的所有子view,然後調用view的measure方法呢,和上面的猜測一樣。但是真的是這樣的嗎?全局搜尋源碼發現,僅有少數類(ViewGroup的子類)的onMeasure方法會調用measureChildren方法,例如, StackView, AbsoluteLayout等,其他如FrameLayout, LinearLayout, RelativeLayout, AbsoluteLayout等類不會調用該方法。

1.2 onLayout

measure過程結束後,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用于給視圖進行布局的,也就是确定視圖的位置。ViewRoot的performTraversals()方法會在measure結束後繼續執行,調用performLayout并調用View的layout()方法來執行此過程,如下所示:

final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
           

layout()方法接收四個參數,分别代表着左、上、右、下的坐标,當然這個坐标是相對于目前視圖的父視圖而言的。可以看到,這裡還把剛才測量出的寬度和高度傳到了layout()方法中。

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

重繪時調用View的onLayout方法,但是onLayout是一個空方法,哪裡出問題了呢? 因為onLayou是為了确定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置。是以應該是ViewGroup(未實作)以及其子類會實作該方法。例如, FrameLayout的onLayout方法如下,

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
              final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
           

LayoutParams是子view期望在ViewGroup中的位置,比如,距離子view頂部多少等等資訊,和width等資訊完全不同,不要混淆。

1.3 ondraw

measure和layout的過程都結束後,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這裡才真正地開始對視圖進行繪制。ViewRootImpl中的drawSoftware方法會擷取一個Canvas對象,然後調用view的draw方法,

mView.draw(canvas);
           

view的draw方法主要分為6步, 第二步和第五步在一般情況下很少用到,是以僅分析剩下的4個步驟,

// Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas); // 繪制背景
        }

// Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas); // 繪制view

            // Step 4, draw the children
            dispatchDraw(canvas); // 繪制子view

// Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas); // 繪制前景
           

1.3.1 drawBackground

繪制view的背景

private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();

        // Attempt to use a display list if requested.
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mHardwareRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.isValid()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }

        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
           

這裡會先得到一個Drawable對象,然後根據layout過程确定的視圖位置來設定背景的繪制區域,之後再調用Drawable的draw()方法來完成背景的繪制工作。那麼這個mBackground對象是從哪裡來的呢?其實就是在XML中通過android:background屬性設定的圖檔或顔色。當然也可以在代碼中通過setBackgroundColor()、setBackgroundResource()等方法進行指派。

1.3.2 onDraw

onDraw方法會對視圖的内容進行繪制,View的onDraw方法是一個空方法, 因為每個視圖的内容部分肯定都是各不相同的,這部分的功能交給子類來去實作也是理所當然的。TextView、ImageView等類的源碼,它們都有重寫onDraw方法,并且在裡面執行了相當不少的繪制邏輯。繪制的方式主要是借助Canvas這個類,它會作為參數傳入到onDraw方法中,供給每個視圖使用。Canvas這個類的用法非常豐富,基本可以把它當成一塊畫布,在上面繪制任意的東西.

1.3.3 dispatchDraw

View的dispatchDraw又是個空方法,一般包含子view的ViewGroup才會實作該方法。在這個方法中,會調用drawChild,逐個繪制子view

for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);
       if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
           
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
           

1.3.4 onDrawForeground

繪制前景和繪制背景幾乎是一模一樣的,都是調用Drawable的draw方法完成的。

繼續閱讀