「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」
相关文章:
Android View的绘制流程(一)-绘制流程以及Activity视图介绍
Android View的绘制流程(二)-Activity视图-DecorView
Android View的绘制流程(三)-Activity视图-WindowManager
Android View的绘制流程(四)-Activity视图-ViewRootImpl
Android View的绘制流程(五)-Measure
Android View的绘制流程(六)-Layout
Android View的绘制流程(七)-Draw
上一篇文章把 View的绘制流程中的测量(Mearsure)的逻辑通过介绍源码中的主要内容分析了一下,主要分为View和View的子类测量,最后返回整体的尺寸,重要部分是MearsureSpec,mearsure和onMessure方法的理解,在后续的自定义View会经常使用到,也是解决View显示问题的重要方面。
接下来介绍 View的绘制流程中的Layout部分,在 ViewRootImpl 中,调用 performLayout 方法来确定 DecorView 在屏幕中的位置,下面看下具体的代码逻辑:
// ViewRootImpl
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
// 根据测量结果进行绘制
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
// requestLayout() was called during layout.
// If no layout-request flags are set on the requesting views, there is no problem.
// If some requests are still pending, then we need to clear those flags and do
// a full request/measure/layout pass to handle this situation.
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
// Set this flag to indicate that any further requests are happening during
// the second pass, which may result in posting those requests to the next
// frame instead
mHandlingLayoutInLayoutRequest = true;
// Process fresh layout requests, then measure and layout
int numValidRequests = validLayoutRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during layout: running second layout pass");
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mHandlingLayoutInLayoutRequest = false;
// Check the valid requests again, this time without checking/clearing the
// layout flags, since requests happening during the second pass get noop'd
validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
if (validLayoutRequesters != null) {
final ArrayList<View> finalRequesters = validLayoutRequesters;
// Post second-pass requests to the next frame
getRunQueue().post(new Runnable() {
@Override
public void run() {
int numValidRequests = finalRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = finalRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during second layout pass: posting in next frame");
view.requestLayout();
}
}
});
}
}
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
代码主要内容,首先会收到layout的请求,请求layout需要判断是否都是有效的请求,如果是有效的,那么就必须先处理,清除标记位(View.PFLAG_FORCE_LAYOUT)这些标记,再做一次彻底的重绘工作:measure,layout,layout的含义就是布局,对于DecorView来说,layout就是对本事的view进行布局,四个参数,分别表示左上右下四个位置,layout代码如下:
// ViewGroup
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
可以看出来,viewGroup的layout是final类型,所以最终是调用父类的layout方法,也就是view的layout方法。
// View
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;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 设置界面的显示大小
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 发生了改变,或者是需要重新 layout,那么就会进入 onLayout 逻辑
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 开始
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
// 清除请求 layout 的标记
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);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);最终调用setFram方法,把左上右下四个参数传递过去:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
// 有一个不一样说明发生了改变
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);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
// 做好标记
mPrivateFlags |= PFLAG_HAS_BOUNDS;
// size 改变的回调
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
// If we are visible, force the DRAWN bit to on so that
// this invalidate will go through (at least to our parent).
// This is because someone may have invalidated this view
// before this call to setFrame came in, thereby clearing
// the DRAWN bit.
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
// parent display list may need to be recreated based on a change in the bounds
// of any child
invalidateParentCaches();
}
// Reset drawn bit to original value (invalidate turns it off)
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
传递过去的四个值,进行了重新赋值,赋值后对于每个View或者ViewGroup来说就是对应的布局中的位置,也就是最后的宽高大小,如果想要得到View的位置信息,必须在layout执行完才能拿到相应的数据,否则返回的都是0,当初始化结束后,布局的工作也就完成了,赋值后会做前后对比,如果发生改变就会调用sizeChange方法,然后回调onSizeChanged(),通过boolean判断是否发生变化,第一次 laout 的时候也会调用。在自定义view 的时候,可以在这里获取控件的宽和高度。
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
......
}
上面确定完DecorView的位置后,就会layout子View的位置,对于 onLayout 方法,View 和 ViewGroup 类是空实现,接下来看 FrameLayout 的实现:
// FrameLaout
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
// 获取 DecoverView 剩余的空间范围
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
// DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START 默认是在左上角
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;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
上面代码主要做了哪些事呢,首先火枪父容器的内边距也就是padding值,这样可以确定DecorView所要显示的空间,然后通过遍历的方式,根据子 View 的 layout_gravity 属性、子 View 的测量宽高、父容器的 padding 值、来确定子 View 的左上角的坐标位置,最后调用child.layout 方法,参数是左上角坐标和自身宽高结合起来的,这样就可以确定子 View 的位置。
如果子 View 是一个 ViewGroup,那么就会重复以上步骤,如果是一个 View,那么会直接调用 View的layout 方法,在该方法内部会设置 view 的四个布局参数,接着调用 onLayout 方法,此方法需要子类去实现,不同的 View 实现方式不同,这里就不分析了。
layout阶段的基本思想也是由根View开始,递归地完成整个控件树的布局(layout)工作。总结下View绘制流程中layout的整体流程:
这里再简单介绍下,我们开发过程中常用的获取宽高的方法使用场景:
1、获取View测量的宽高:getMeasuredWidth(),getMeasuredHeight(),这2个方法的调用时机在measure过程中;可以在onLayout中调用上面方法获取值,
2、获取View的最终宽高:getWidth(),getHeight(),这2个方法的调用时机在layout过程中,可以在onLayout之后调用上面方法。
一般情况下,上面2个情况的宽高的值是一样的,在非一般情况下,也就是通过人为设置,重写View的layout()强行设置,这种情况下,测量的值与最终的值是不一样的。
以上就是对View绘制过程中layout的介绍,有理解错误的地方,还请指正,欢迎留言评论