[置頂] 從源碼角度分析view的layout過程


    上兩篇文章主要講述了view的measure過程,主要分析了xml文件中控件的height和width設置成不同值的時候,經過測量之后,如何計算出控件的真實高度。所以也就驗證了我們經常所說的measure過程就是把match_parent等值轉化成在具體設備上的具體的值。
    本文主要分析一下layout的過程,同樣我們以LinearLayout的layout過程為例。
    在ViewRoot的performTraversals方法中首先是measure過程,然后接着是layout,layout開始也是從host.layout方法開始的。

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

    host是一個viewGroup,ViewGroup繼承自view,然而view中的layout是final的,所以這里調用的仍然是view的layout方法,然后調用了onLayout方法,所以我們從linearLayout的onLayout方法中開始分析

public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
}

    linearLayout的layout過程如下:

    @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical();
} else {
layoutHorizontal();
}
}
void layoutVertical() {
final int paddingLeft = mPaddingLeft;
int childTop = mPaddingTop;
int childLeft;
final int width = mRight - mLeft;
int childRight = width - mPaddingRight;
// Space available for child 實際上可用的寬度空間
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
//根據LinearLayotu的gravity的值計算childTop的位置
if (majorGravity != Gravity.TOP) {
switch (majorGravity) {
case Gravity.BOTTOM:
childTop = mBottom - mTop + mPaddingTop - mTotalLength;
break;
case Gravity.CENTER_VERTICAL:
childTop += ((mBottom - mTop) - mTotalLength) / 2;
break;
}
}
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();
//拿到子View的LayoutParams
final LinearLayout.LayoutParams lp =(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
//計算子View在水平方向的childLeft
switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.LEFT:
childLeft = paddingLeft + lp.leftMargin;
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
default:
childLeft = paddingLeft;
break;
}
childTop += lp.topMargin;
// 調用child.layout方法設置child的布局位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}

首先根據mRight - mLeft獲得width,根據width - paddingLeft - mPaddingRight獲得childSpace,也就是實際上我們可以用的寬度。然后根據LinearLayotu的gravity的值計算childTop的位置:

  1. 為Gravity.BOTTOM:
    childTop = mBottom - mTop + mPaddingTop - mTotalLength;
    在這里當測量出來的mTotalLength總高度足夠大的時候會出現childTop是負數的情況,也就是child的上面一部分會顯示不全的情況,對應的圖就像是下面的情況,圖中上面的藍色框內的一部分是不會顯示的。
    這里寫圖片描述
  2. 為Gravity.CENTER_VERTICAL:
    childTop += ((mBottom - mTop) - mTotalLength) / 2;
    對應的圖片如下所示:
    這里寫圖片描述

        接着循環遍歷子view,如果子view為可見的,則計算出子view在水平方向上的childLeft,這里我們討論的是垂直方向的布局,這里為什么會出現還要計算view在水平方向上的left呢,因為就算是垂直布局,每個子view他們也有marginleft和paddingLeft值。最后調用setChildFrame設置子view的布局位置,我們可以進入此方法看一下:

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

public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
}

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 drawn = mPrivateFlags & DRAWN;
invalidate();
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mPrivateFlags |= HAS_BOUNDS;
int newWidth = right - left;
int newHeight = bottom - top;
if (newWidth != oldWidth || newHeight != oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) {
mPrivateFlags |= DRAWN;
invalidate();
}
// Reset drawn bit to original value (invalidate turns it off)
mPrivateFlags |= drawn;

mBackgroundSizeChanged = true;
}
return changed;
}

給子View布局最終調用的是setFrame方法,四個參數分別代表view在其父視圖中的位置。首先判斷如果這四個值和之前已經有的值是否相等,如果有一個不相等,就代表要重新布局,此時如果子view現在的高度和寬度和之前的寬高是不相同的,那么就必須要調用onSizeChanged方法,通知程序view的大小發生了變化,最后如果view是VISIBLE的,那么就要執行invalidate操作。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com