View的Layout過程解析

1、問題

《View的Measure過程解析》中分析了View的大小測量過程,View的大小確定後,就是確定View在父容器中的位置,接下來我們就來分析View的佈局流程。

2、分析

與分析View的Measure過程一樣,從ViewRootImpl.performTraversals( )方法開始分析View的Layout過程,我們在該方法中可找到Layout的入口performLayout( )方法。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    mLayoutRequested = false;
    mScrollMayChange = true;
    mInLayout = true;

    // mView爲DecorView
    final View host = mView;

    try {
        // 調用DecorView的layout方法進行佈局,host.getMeasuredWidth()得到View的寬度(在Measure過程後,該值就可確定了)
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

        . . . (省略)

    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}

DecorView繼承自View,但DecorView沒重寫layout( )這個方法,所以調用host.layout( )方法最終還是執行View.layout( )方法,該方法用於確定自己在佈局中的位置

// l爲DecorView左邊框距父容器左邊框的像素大小
// t爲DecorView頂部距父容器頂部的像素大小 
// r爲DecorView右邊框距父容器右邊框的像素大小
// b爲DecorView底部距父容器底部的像素大小
public void layout(int l, int t, int r, int b) {

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

    // setFrame(l, t, r, b)方法是設置DecorView的位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

        // 設置DecorView的子View的位置
        onLayout(changed, l, t, r, b);

        // 通知已註冊的監聽器
        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);
            }
        }
    }
}

看一下setFrame()的實現

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

        // 確定大小是否有變化,如果大小變化了,則調用invalidate()觸發一次重新佈局操作
        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);

        // 設定View的位置,在RenderNode中記錄位置信息,canvas就是根據這些位置信息繪製View的
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

        mPrivateFlags |= PFLAG_HAS_BOUNDS;

        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
        ...
    }
    return changed;
}

當DecorView設定了自身的位置後,接着需要確定DecorView中子View的位置,確定子View的位置是通過調用onLayout( )完成。因爲一個View對象是不存在子View的,所以View.onLayout( )不實現任何操作,只有容器類纔會實現onLayout( )這個方法。由於每種類型的佈局容器都有不同的佈局方式,所以它們對於OnLayout( )的實現方式都是不一樣的,由於DecorView繼承自FrameLayout,現在就來看一下FrameLayout中的onlayout( )方法的實現。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false);
}

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

    // 減去padding值後,計算出剩餘的用於展示子View的區域邊界值,這些值是以父View的左上角爲座標的原點,如下圖所示
    final int parentLeft = getPaddingLeftWithForeground(); 
    final int parentRight = right - left - getPaddingRightWithForeground();
    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    // 這裏就會遍歷該父View的直接子View
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // 獲取子View的測量寬高,這兩值是Measure過程測量的結果
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            // lp.gravity的值是子View在佈局文件中layout_gravity屬性值
            int gravity = lp.gravity;
            ...
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

            // 根據子View在佈局文件中設置水平對齊方式計算子View相對父View的左邊距
            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;
            }

            // 根據子View在佈局文件中設置的垂直方向上的對齊方式計算子View相對於父View頂部邊距
            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;
            }
            // 最後調用layout()方法設定子View的位置
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

當調用layout()方法時,該方法會做兩件事情:

1. 確定自己的位置
2. 如果本身是個ViewGroup時,繼續確定子View的位置

所以當調用child.layout( )時會確定子View以及子View包含的View的位置,就這樣一級一級遞歸下去,知道子View不是ViewGroup類型,纔會停止遞歸,所以上面的佈局過程其實就是幾個深度遍歷的整個View數的過程。並在遍歷過程中確定View的位置。
這裏寫圖片描述

3、總結

sdk提供了幾種佈局容器,每種佈局容器都會重寫onLayout(),並根據自己的佈局方式來決定子View的位置,由於FrameLayout的佈局相對來說比較簡單,所以上面使用FrameLayout的佈局過程作爲分析的案例,對於其它的佈局的onLayout方法我就不再分析了,感興趣的可以自己起看一下源碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章