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方法我就不再分析了,感興趣的可以自己起看一下源碼。