提示:本文的源码均取自Android 7.0(API 24)
自定义View是Android进阶路线上必须攻克的难题,而在这之前就应该先对View的工作原理有一个系统的理解。本系列将分为4篇博客进行讲解,本文主要对View的布局流程进行讲解。相关内容如下:
- Android View原理解析之基础知识(MeasureSpec、DecorView、ViewRootImpl)
- Android View原理解析之测量流程(measure)
- Android View原理解析之布局流程(layout)
- Android View原理解析之绘制流程(draw)
在本系列的第一篇文章中讲到整个视图树(ViewTree)的根容器是DecorView,ViewRootImpl通过调用DecorView的layout方法开启布局流程。layout是定义在View中的方法,我们先从View的角度来看看布局过程中发生了什么。
首先来看一下layout
方法中的逻辑,关键代码如下:
/**
* 通过这个方法为View及其所有的子View分配位置
*
* 派生类不应该重写这个方法,而应该重写onLayout方法,
* 并且应该在重写的onLayout方法中完成对子View的布局
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
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;
// ① 通过setOpticalFrame或setFrame为View设置坐标,并判断位置是否发生改变
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// ② 如果位置发生了改变,就调用onLayout方法完成布局逻辑
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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);
}
}
}
.......
}
layout方法和measure不同,并没有使用final修饰,但注释中也清清楚楚地写着View的派生类不应该重写这个方法,而应该重写onLayout方法,并且应该在重写的onLayout方法中完成对子View的布局逻辑。
可以看到,在代码①的位置先通过setOpticalFrame
或setFrame
方法为View设置left、right、top、bottom坐标,并记录View的位置相比之前是否发生了变化。setOpticalFrame最终也调用了setFrame方法,只是在这之前对传入的四个参数做了一些更改。setFrame中的主要逻辑其实就是将传入的四个参数分别赋值给View的四个坐标,并且计算View当前的宽高,最后判断位置是否发生了改变(只要四个坐标中的任何一个值发生了变化都会返回true)。那就让我们来看看setFrame中发生了什么吧:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
// (A) 只要任何一个坐标不同就认为View的位置发生了变化
changed = true;
int drawn = mPrivateFlags & PFLAG_DRAWN;// Remember our drawn bit
// (B) 计算旧的宽高和新的宽高
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
// 如果宽高与原来不同就认为View的大小发生了变化
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// 执行重绘流程
invalidate(sizeChanged);
// (C) 为坐标赋新的值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
// (D) 通知View的大小发生变化(最终会调用onSizeChanged方法)
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;
}
这个方法中的逻辑还是很清晰的,首先在代码(A)的位置记录View的位置是否发生改变,然后在代码(B)的位置通过旧坐标和传入的新坐标分别计算View的旧宽高和新宽高,如果两者不同就认为View的大小发生了变化(sizeChanged
)。紧接着在代码(C)的位置将传入的参数赋值给View的四个坐标,到了这一步View的位置信息就真正发生变化了。最后在代码(D)的位置,如果sizeChanged为true,就调用sizeChange方法。View#onSizeChanged
方法将在这里调用,通知View的大小已经发生改变。View#onSizeChanged
是一个空方法,子类可以重写这个方法实现自己的逻辑。
执行完上面的步骤后,如果View的位置发生了改变,将在layout代码②的位置调用onLayout
方法完成对子View的布局逻辑,这个方法的代码如下:
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* 派生类应该重写这个方法,并且完成对子View的布局逻辑
*
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
可以看到在View中onLayout是个空方法,因为View自身的布局逻辑已经在setFrame
方法中完成了,这里是要完成对子View的布局逻辑。但是对于一个纯粹的View而言,它是没有子View的,所以这里自然什么都不用做。
因此,如果我们通过继承View实现自定义View,理论上是不需要重写layout和onLayout方法的,使用系统默认实现就好了。
讲完了View中的布局逻辑,现在我们再切换到ViewGroup的角度来看看layout流程中都要做些什么。
首先依旧是layout方法,ViewGroup#layout
方法代码如下:
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
// 调用View的layout方法
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
虽然ViewGroup重写了layout方法,但是关键逻辑依旧是通过调用View#layout实现的,咱们就不在这里耗费时间了,直接看ViewGroup#onLayout
方法:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
当我们兴冲冲地找到onLayout方法,才发现这却是一个抽象方法。静下心来想想,ViewGroup作为布局容器的抽象父类,其实是无法提供一个通用布局逻辑的,这一工作只能交给ViewGroup的具体子类实现。但是作为布局容器,必须要实现对子View的布局逻辑,所以ViewGroup将onLayout标记为抽象方法,保证它的子类一定会实现这个方法。
如果我们通过继承ViewGroup的方式实现自定义View,就必须要实现onLayout方法。常规套路就是循环处理子View,根据希望的布局方式计算每个子View的坐标,然后调用子View的layout方法传入计算好的坐标。如果子View也是一个ViewGroup的话,又会在onLayout方法中继续调用它的子View的layout方法,布局流程就这样从顶级容器逐渐向下传播了。
上面分别从View和ViewGroup的角度讲解了布局流程,这里再以流程图的形式归纳一下整个layout过程,便于加深记忆:
和上一篇文章中的测量流程相比,本文的内容相对简单一点,但仅仅依靠阅读很难形成深刻的记忆。不妨打开AndroidStudio,循着本文的脉络试着一步步探索源码中的逻辑,学习效果可能会更好。
https://blog.csdn.net/lfdfhl/article/details/51393131
https://blog.csdn.net/a553181867/article/details/51524527