目錄
作用
根據 measure 測量出來的寬高,確定所有 View 的位置。
具體分析
View 本身的位置是通過它的四個點來控制的:
以下涉及到源碼的部分都是版本27的,為方便理解觀看,代碼有所刪減。
layout 的流程
先通過 measure 測量出 ViewGroup 寬高,ViewGroup 再通過 layout 方法根據自身寬高來確定自身位置。當 ViewGroup 的位置被確定后,就開始在 onLayout 方法中調用子元素的 layout 方法確定子元素的位置。子元素如果是 ViewGroup 的子類,又開始執行 onLayout,如此循環往復,直到所有子元素的位置都被確定,整個 View 樹的 layout 過程就執行完了。
在上一節 《View的繪制-measure流程詳解》中說過,View 的繪制流程是從 ViewRootViewImpl 中的 performMeasure()
、performLayout
、performDraw
開始的。在執行完 performMeasure()
后,開始執行 performLayout
方法:(以下源碼有所刪減)
//ViewRootViewImpl 類
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {final View host = mView;/*代碼省略*///開始執行 View 的 layout 方法host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());/*代碼省略*/
}
復制代碼
如此,就執行了 View 的 layout 方法:
//View 類
public void layout(int l, int t, int r, int b) {/*代碼省略*/int oldL = mLeft;int oldT = mTop;int oldB = mBottom;int oldR = mRight;//確定 View 的四個點后,調用 setOpticalFrame/setFrame 方法來控制 View 位置。//方法 1--->setOpticalFrame//方法 2--->setFrameboolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {//執行 onLayout 方法onLayout(changed, l, t, r, b);/*代碼省略*/}/*代碼省略*/
}//方法 1---->setOpticalFrame
//View 類
private boolean setOpticalFrame(int left, int top, int right, int bottom) {Insets parentInsets = mParent instanceof View ?((View) mParent).getOpticalInsets() : Insets.NONE;Insets childInsets = getOpticalInsets();//內部最終也是調用 setFrame 方法return setFrame(left + parentInsets.left - childInsets.left,top + parentInsets.top - childInsets.top,right + parentInsets.left + childInsets.right,bottom + parentInsets.top + childInsets.bottom);
}//方法 2--->setFrame
//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 positioninvalidate(sizeChanged);//對 View 的四個點賦值mLeft = left;mTop = top;mRight = right;mBottom = bottom;mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);mPrivateFlags |= PFLAG_HAS_BOUNDS;if (sizeChanged) {//這里 sizeChange 方法內部調用了 onSizeChanged 方法。//所以當控件的大小和位置改變的時候會回調 onSizeChanged 方法//方法 3--->sizeChangesizeChange(newWidth, newHeight, oldWidth, oldHeight);}/*代碼省略*/}return changed;
}//方法 3--->sizeChange
// View 類
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {//執行 onSizeChanged 方法onSizeChanged (newWidth, newHeight, oldWidth, oldHeight);if (mOverlay != null) {mOverlay.getOverlayView().setRight(newWidth);mOverlay.getOverlayView().setBottom(newHeight);}rebuildOutline();
}
復制代碼
接下來我們再看 onLayout
方法,在 View 中找到 onLayout
方法,會發現這是一個空實現的方法,里面什么也沒有執行,那么我們就在 ViewGroup 中找 onLayout
的實現,發現只是一個抽象方法。
//View 類
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
復制代碼
//ViewGroup 類
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
復制代碼
在 View 類中 onLayout 是一個空實現不難理解,因為如果一個控件繼承了 View ,它是沒有子元素的,不需要確定子元素的位置,只需要確定自己的位置就夠了。
在 ViewGroup 中是一個抽象方法,意思也很明顯了,在控件繼承自 ViewGroup 的時候,我們必須重寫 onLayout
方法。因為如LinearLayout
、RelativeLayout
,他們的布局特性都是不一樣的,需要各自根據自己的特性來進行制定確定子元素位置的規則。
下面以 LinearLayout
為例,分析 onLayout 里面的邏輯。
LinearLayout 的 onLayout 邏輯
//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
和 layoutHorizontal
執行流程類似,就分析layoutVertical
//LinearLayout 類
void layoutVertical(int left, int top, int right, int bottom) {final int paddingLeft = mPaddingLeft;int childTop;int childLeft;// Where right end of child should gofinal int width = right - left;int childRight = width - mPaddingRight;// Space available for childint childSpace = width - paddingLeft - mPaddingRight;final int count = getVirtualChildCount();/*省略代碼*/for (int i = 0; i < count; i++) {//遍歷子 Viewfinal 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();if (hasDividerBeforeChildAt(i)) {childTop += mDividerHeight;}//加上 子元素的 topMargin 屬性值childTop += lp.topMargin;//設置子元素的 位置//方法 1 ----->setChildFramesetChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);/*加上子元素的高度、bottomMargin、偏移量,就是下一個子元素的 初始 top。如此,子元素就從上到下排列,符合我們所知的 LinearLayout 的特性*/childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);i += getChildrenSkipCount(child, i);}}
}//方法 1 ----->setChildFrame
//LinearLayout 類
private void setChildFrame(View child, int left, int top, int width, int height) {//最終又調用了子元素的 layout 方法.child.layout(left, top, left + width, top + height);
}
復制代碼
可以看到,在遍歷子元素后,又調用了子元素的 layout 方法。子元素如果是繼承 ViewGroup,還是會調用到子元素的 onLayout 方法,遍歷自己的子元素,調用自己子元素的 layout 方法,如此循環遞歸,就完成了整個 View 樹的 layout 流程。
getWidth、getMeasureWidth分析
getWidth 獲取的值和 getMeasureWidth 獲取的值有什么不同嗎? 首先看 getWidth 源碼:
//View 類
public final int getWidth() {return mRight - mLeft;
}
復制代碼
然后再看 mRight 和 mLeft 賦值(在我們分析 layout 的時就有展現):
以下為超精簡代碼,哈哈:
//View 類
public void layout(int l, int t, int r, int b) {//setOpticalFrame 最終也是調用了 setFrame 方法boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
}//View 類
protected boolean setFrame(int left, int top, int right, int bottom) {//對四個值進行賦值mLeft = left;mTop = top;mRight = right;mBottom = bottom;
}
復制代碼
然后我們再回顧調用 layout 的方法:
//LinearLayout 類
void layoutVertical(int left, int top, int right, int bottom) {for (int i = 0; i < count; i++) {final int childWidth = child.getMeasuredWidth();final int childHeight = child.getMeasuredHeight();//將獲取的 childWidth 和 childHeight 傳入進去setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);}
}
//LinearLayout 類
private void setChildFrame(View child, int left, int top, int width, int height) {//子元素 layout 的方法中傳入的 right = left + width//子元素 layout 的方法中傳入的 bottom = left + heightchild.layout(left, top, left + width, top + height);
}
復制代碼
這下就一目了然了,在 getWidth()
方法中 mRight - mLeft
其實就是等于 childWidth
,也就是 getWidth() = getMeasureWidth()
。
那么,他們兩個有不相等的時候嗎?有的:
//自定義 View
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {//手動改變傳入的值 super.onLayout(changed, left, top, right+100, bottom+100);
}
復制代碼
如果我們在自定義 View 中重寫 onLayout 方法,并手動改變傳入的值, getWidth()
和 getMeasureWidth()
的值自然就不一樣了。不過這樣做好像沒什么意義?
還有一種情況就是在某些情況下,View 需要多次 measure 才能確定自己的測量寬/高,在前幾次測量的時候,其得出的測量寬/高 ( getMeasureWidth()/ getMeasureHeight()) 和最終寬/高 ( getWidth()/ getHeight()) 可能不一致,但是最終他們的值還是相等的。(這段話摘自剛哥的《Android 開發藝術探索》)
getHeight 和 getMeasuredHeight 過程類似,這里就不分析了。
所以我們在日常開發中,我們可以認為兩者就是相等的。
另:可能還是會有疑惑,這里只分析了 LinearLayout,那么其他布局也適用這個結論么?
FrameLayout 類
//FrameLayout 類
@Override
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();for (int i = 0; i < count; i++) {final View child = getChildAt(i);if (child.getVisibility() != GONE) {final int width = child.getMeasuredWidth();final int height = child.getMeasuredHeight();//從這里看出 FrameLayout 也是適用這個結論的child.layout(childLeft, childTop, childLeft + width, childTop + height);}}
}
復制代碼
RelativeLayout 類
//RelativeLayout 類
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {final int count = getChildCount();for (int i = 0; i < count; i++) {View child = getChildAt(i);if (child.getVisibility() != GONE) {RelativeLayout.LayoutParams st =(RelativeLayout.LayoutParams) child.getLayoutParams();//Relativelayout 這里傳入的是 LayoutParams 中的屬性//st.mRight 和 st.mBottom 賦值很復雜,不過他們也是適用這個結論的,具體可以查看源碼分析child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);}}
}
復制代碼
其他我們日常開發使用的布局也是適用于這個結論的。
總結
參考文獻
《Android開發藝術探索》第四章-View的工作原理
自定義View Layout過程 - 最易懂的自定義View原理系列(3)
Android開發之getMeasuredWidth和getWidth區別從源碼分析