上篇介绍了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赋值。