上篇介紹了ViewRootImpl調用View的測量操作,下面就開始介紹ViewRootImpl中的佈局操作了。我們還是從ViewRootImpl中的performLayout開始。
ViewRootImpl#performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true; //標記開始佈局
final View host = mView;
. . . . . . . . .
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
. . . . . . . . .
mInLayout = false; //標記佈局結束
}
該方法中的核心方法是調用View中的layout。
View#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;
}
// 保存上次View的四個位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 設置當前視圖View的左,頂,右,底的位置,並且判斷佈局是否有改變
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果佈局有改變,則視圖View重新佈局
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;
}
方法中,首先判斷是否需要重新進行測量,然後保存佈局的四個位置。調用setFrame方法來設置View的佈局位置。
View#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;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
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的最新位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
//如果當前View的尺寸有所變化
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
. . . . . . . . .
}
return changed;
}
方法中判斷當前View的視圖的位置與上次不一致時,則View會重新佈局。調用方法invalidate
清除上次的位置,然後保存最新的View的位置。
完成setFrame來設置view的位置之後,就會繼續調用onLayout(changed, l, t, r, b)方法。
View#onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
可以看到該方法是個空方法,具體的實現是在子類中。我們知道DecorView是繼承自FrameLayout類,我們從該類中查看:
FrameLayout#onLayout
@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();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
//遍歷子view
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//過濾掉Visibility爲Gone的情況
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//獲取子View的寬高
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;
}
//調用子View的佈局
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
從上面的代碼中分析:佈局中的具體邏輯實現是有ViewGroup父View來實現的。遍歷獲得子View的寬高和位置,然後調用child.layout對子視圖View進行佈局操作。
總結:
1.視圖View的佈局邏輯(onLayout)是由父View,也就是ViewGroup的具體子類來實現的。因此,我們如果自定義View一般都無需重寫onMeasure方法,但是如果自定義一個ViewGroup的話,就必須實現onLayout方法,因爲該方法在ViewGroup是抽象的,所有ViewGroup的所有子類必須實現onLayout方法。
2.當我們的視圖View在佈局中使用 android:visibility=”gone” 屬性時,是不佔據屏幕空間的,因爲在佈局時ViewGroup會遍歷每個子視圖View,判斷當前子視圖View是否設置了 Visibility==GONE,如果設置了,當前子視圖View就會添加到父容器上,因此也就不佔據屏幕空間。
3.必須在View調用layout()之後調用getHeight()和getWidth()方法獲取到的View的寬高才大於0。因爲只有在View調用setFrame時,纔會給mLeft,mRight,mTop,mBottom賦值。