View的绘制流程 onLayout
onLayout的原理
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
host.getLocationInWindow(mTmpLocation);
mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
mTmpLocation[0] + host.mRight - host.mLeft,
mTmpLocation[1] + host.mBottom - host.mTop);
host.gatherTransparentRegion(mTransparentRegion);
if (mTranslator != null) {
mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
}
if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
mPreviousTransparentRegion.set(mTransparentRegion);
mFullRedrawNeeded = true;
try {
mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
} catch (RemoteException e) {
}
}
}
}
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
//分发内部的insets,这里我们暂时不去关心省略的逻辑
...
}
接下来performTraversals后续事情分为如下几个方面:
- 1.就是判断当前的View是否需要重新摆放位置。如果通过requestLayout执行performTraversals方法,则layoutRequested为true;此时需要调用performLayout进行重新的摆放。
- 2.判断到调用了requestTransparentRegion方法,需要重新计算透明区域,则会调用gatherTransparentRegion方法重新计算透明区域。如果发现当前的和之前的透明区域发生了变化,则通过WindowSession更新WMS那边的区域。
这种情况通常是指存在SurfaceView的情况。因为SurfaceView本身就拥有自己的一套体系沟通到SF体系中进行渲染。Android没有必要把SurfaceView纳入到层级中处理,需要把这部分当作透明,当作不必要的层级进行优化。
整个核心我还是回头关注performLayout究竟做了什么?
ViewRootImpl performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
mHandlingLayoutInLayoutRequest = true;
int numValidRequests = validLayoutRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mHandlingLayoutInLayoutRequest = false;
}
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
其实整个核心还是这一段代码:
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
这一段代码将会开启遍历View树的layout的流程,也就是View的摆放的流程。
当处理完layout流程之后,就会继续检查是否有View在测量,摆放的流程请求
中是否有别的View请求进行刷新,如果请求则把这个View保存在mLayoutRequesters对象中。此时取出重新进行测量和摆放。
记住此时的根布局是DecorView是layout方法.由于DecorView和FrameLayout都有重写layout,我们来看看ViewGroup的layout.
ViewGroup layout
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;
}
}
能看到,layout走到View的layout方法的条件有2:
- 1.mSuppressLayout为false,也就是不设置抑制Layout方法
- 2.mTransition LayoutTransition 布局动画为空或者没有改变才可以。
在Android中动画api中提供了LayoutTransition,用于对子View的加入和移除添加自定义的属性动画。
记住此时从DecorView传进来的layout四个参数,分别代表该View可以摆放的左部,顶部,右部,底部四个位置。但是不代表该View就是摆放到这个位置。
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;
}
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);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b)
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
} 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()) {
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);
}
}
在这里可以分为如下几个步骤:
- 1.判断PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT是否开启了。这个标志位打开的时机是在onMeasure步骤发现原来父容器传递下来的大小不变,就会设置老的测量结果在View中。在layout的步骤会先调用一次onMeasure继续遍历测量底层的子View的大小。
- 2.判断isLayoutModeOptical是否开启了光学边缘模式。打开了则setOpticalFrame进行四个方向的边缘设置,否则则setFrame处理。用于判断是否需要更新四个方向的数值。
- 3.如果发生了大小或者摆放的位置变化,则进行onLayout的回调。一般子类都会重写这个方法,进行进一步的摆放设置。
- 4.如果需要显示滑动块,则初始化RoundScrollbarRenderer对象。这个对象实际上就一个封装好如何绘制绘制一个滑动块的自定义View。
- 5.回调已经进行了Layout变化监听的OnLayoutChangeListener回调。
- 6.当前View的layout的行为进行的同时没有另一个layout进行,说明当前的Layout行为是有效的。如果layout的行为是无效的,此时的View又获取了焦点则清除。如果此时是想要请求焦点,则清空焦点。
- 7.通知AllFillManager进行相关的处理。
这里我们着重看看setFrame方法做了什么。
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;
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(sizeChanged);
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);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
invalidateParentCaches();
}
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
就以setFrame为例子来看看其核心思想。实际上很简单:
- 1.比较左上右下四个方向的数值是否发生了变化。如果发生了变化,则更新四个方向的大小,并判断整个需要绘制的区域是否发生了变化,把sizechange作为参数调用invalidate进行onDraw的刷新。
- 2.获取mRenderNode这个硬件渲染的对象,并且设置这个渲染点的位置。
- 3.调用sizeChange方法进行onSizeChange的回调:
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
if (mOverlay != null) {
mOverlay.getOverlayView().setRight(newWidth);
mOverlay.getOverlayView().setBottom(newHeight);
}
if (!sCanFocusZeroSized && isLayoutValid()
// Don't touch focus if animating
&& !(mParent instanceof ViewGroup && ((ViewGroup) mParent).isLayoutSuppressed())) {
if (newWidth <= 0 || newHeight <= 0) {
if (hasFocus()) {
clearFocus();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).clearFocusedInCluster();
}
}
clearAccessibilityFocus();
} else if (oldWidth <= 0 || oldHeight <= 0) {
if (mParent != null && canTakeFocus()) {
mParent.focusableViewAvailable(this);
}
}
}
rebuildOutline();
}
如果当前不是ViewGroup且新的宽高小于0焦点则清除焦点,并且通知AccessibilityService。如果新的宽高大于0,则通知父容器焦点可集中。最后重新构建外框。
- 4.如果判断到mGhostView不为空,且当前的View可见。则对mGhostView发出draw的刷新命令。并通知父容器也刷新。这里mGhostView实际上是一层覆盖层,作用和ViewOverLay相似。
到这里View和ViewGroup的onLayout似乎就看完了。但是还没有完。记得我们现在分析的是DecorView。因此我们看看DecorView在onLayout中做了什么。
DecorView onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
getOutsets(mOutsets);
if (mOutsets.left > 0) {
offsetLeftAndRight(-mOutsets.left);
}
if (mOutsets.top > 0) {
offsetTopAndBottom(-mOutsets.top);
}
if (mApplyFloatingVerticalInsets) {
offsetTopAndBottom(mFloatingInsets.top);
}
if (mApplyFloatingHorizontalInsets) {
offsetLeftAndRight(mFloatingInsets.left);
}
updateElevation();
mAllowUpdateElevation = true;
if (changed && mResizeMode == RESIZE_MODE_DOCKED_DIVIDER) {
getViewRootImpl().requestInvalidateRootRenderNode();
}
}
1.先调用了FrameLayout的onLayout的方法后,确定每一个子View的摆放位置。
2.getOutsets方法获取mAttachInfo中的mOutSet区域。从上一篇文章的打印看来,mOutsets的区域实际上就是指当前屏幕最外层的四个padding大小。如果左右都大于0,则调用offsetLeftAndRight和offsetTopAndBottom进行设置。
3.如果mApplyFloatingVerticalInsets或者mApplyFloatingHorizontalInsets为true,说明DecorView自己需要处理一次onApplyWindowInsets的回调。如果关闭FLAG_LAYOUT_IN_SCREEN标志位也就是非全屏模式,且宽或高是WRAP_CONTENT模式,则给横轴或者纵轴两端增加systemwindowInset的padding数值。
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
final WindowManager.LayoutParams attrs = mWindow.getAttributes();
mFloatingInsets.setEmpty();
if ((attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0) {
if (attrs.height == WindowManager.LayoutParams.WRAP_CONTENT) {
mFloatingInsets.top = insets.getSystemWindowInsetTop();
mFloatingInsets.bottom = insets.getSystemWindowInsetBottom();
insets = insets.inset(0, insets.getSystemWindowInsetTop(),
0, insets.getSystemWindowInsetBottom());
}
if (mWindow.getAttributes().width == WindowManager.LayoutParams.WRAP_CONTENT) {
mFloatingInsets.left = insets.getSystemWindowInsetTop();
mFloatingInsets.right = insets.getSystemWindowInsetBottom();
insets = insets.inset(insets.getSystemWindowInsetLeft(), 0,
insets.getSystemWindowInsetRight(), 0);
}
}
mFrameOffsets.set(insets.getSystemWindowInsets());
insets = updateColorViews(insets, true /* animate */);
insets = updateStatusGuard(insets);
if (getForeground() != null) {
drawableChanged();
}
return insets;
}
- 4.当所有都Layout好之后,则调用updateElevation更新窗体的阴影面积。
// The height of a window which has focus in DIP.
private final static int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
// The height of a window which has not in DIP.
private final static int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
private void updateElevation() {
float elevation = 0;
final boolean wasAdjustedForStack = mElevationAdjustedForStack;
final int windowingMode =
getResources().getConfiguration().windowConfiguration.getWindowingMode();
if ((windowingMode == WINDOWING_MODE_FREEFORM) && !isResizing()) {
elevation = hasWindowFocus() ?
DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP : DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP;
if (!mAllowUpdateElevation) {
elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
}
elevation = dipToPx(elevation);
mElevationAdjustedForStack = true;
} else if (windowingMode == WINDOWING_MODE_PINNED) {
elevation = dipToPx(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP);
mElevationAdjustedForStack = true;
} else {
mElevationAdjustedForStack = false;
}
if ((wasAdjustedForStack || mElevationAdjustedForStack)
&& getElevation() != elevation) {
mWindow.setElevation(elevation);
}
}
能看到这个过程中,如果窗体模式是freedom模式(也就是更像电脑中可以拖动的窗体一样)且不是正在拖拽变化大小,则会根据是否窗体聚焦了来决定阴影的的四个方向的大小。注意如果是没有焦点则为5,有焦点则为20.这里面的距离并非是measure的时候增加当前测量的大小,而是在测量好的大小中继续占用内容空间,也就是相当于设置了padding数值。
如果窗体是WINDOWING_MODE_PINNED模式或者WINDOWING_MODE_FREEFORM模式,且elevation发生了变化则通过PhoneWindow.setElevation设置Surface的Insets数值。
- 5.最后调用requestInvalidateRootRenderNode,通知ViweRootImpl中的硬件渲染对象ThreadRenderer进行刷新绘制。
整个过程中,有一系列函数用于更新摆放的偏移量,就以offsetLeftAndRight比较重要,看看是如何计算的。
offsetLeftAndRight
public void offsetLeftAndRight(int offset) {
if (offset != 0) {
final boolean matrixIsIdentity = hasIdentityMatrix();
if (matrixIsIdentity) {
if (isHardwareAccelerated()) {
if (!matrixIsIdentity) {
invalidateViewProperty(false, true);
}
invalidateParentIfNeeded();
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
这个过程会判断offset如果不等于0才会进行计算。如果从RenderNode判断到存在单位变化矩阵(关于这个矩阵我们暂时不去聊,涉及到了硬件渲染的机制)。
- 1.判断到如果有硬件加速,则直接调用invalidateViewProperty方法刷新。
- 2.没有硬件加速,软件渲染的逻辑本质上也是一样的。
在这里能看到如果offset小于0,则把minLeft的大小增加。如果offset大于0,则增加maxRight的数值。计算出刷新的区域:
offset>0 maxRight = maxRight + mRight
offset<0 minLeft = minLeft + mLeft
刷新横向范围:maxRight - minLeft
计算出需要刷新的区域通过获取父布局的invalidateChild发送刷新命令。换算成图就是如下原理:
- 3.如果没有单位变换矩阵,则调用invalidateViewProperty发送刷新命令
- 4.最后mLeft和mRight增加offset。并把left和right同步数据到mRenderNode中。
- 5.如果打开了硬件加速,又一次调用了invalidateViewProperty,并且调用invalidateParentIfNeededAndWasQuickRejected拒绝遍历刷新。
- 6.关闭硬件加速,如果没有单位变换矩阵,则调用invalidateViewProperty。接着调用invalidateParentIfNeeded。
能看到这个过程中有几个方法被频繁的调用:
- 1.invalidate
- 2.invalidateChild
- 3.invalidateViewProperty
- 4.invalidateParentIfNeededAndWasQuickRejected
- 5.invalidateParentIfNeeded
这几个方法决定了绘制需要更新的区域。
FrameLayout onLayout
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();
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;
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);
}
}
}
FrameLayout的onLayout方法很简单。实际上就是遍历每一个可见的子View处理其gravity。
可以分为横轴和竖轴两个方向进行处理:
横轴的处理方向:
- 1.是判断到gravity是CENTER_HORIZONTAL,说明要横向居中:
每一个孩子左侧 = 父View的左侧位置 - (父亲的宽度 - 孩子的宽度)/ 2 + 孩子的marginLeft - 孩子的marginRight
保证孩子位置的居中。
- 2.Gravity.RIGHT:
孩子的左侧= 父View的右侧 - 孩子宽度 - 孩子的marginRight
保证了孩子是从右边还是摆放位置。
- 3.Gravity.LEFT
孩子的左侧= 父View的左侧 + 孩子的marginLeft
竖直方向上同理。
最后把每一个摆放好的孩子位置通过child.layout进行迭代执行子View的layout流程。