6、onLayout源码分析:View的onLayout空实现、ViewGroup的onLayout抽象方法、layout方法的核心逻辑(setFrame + onLayout)
好,咱们继续往下走。上一章我们把 measure 流程啃完了,现在轮到 layout 阶段。
说实话,layout 这个阶段在面试里问得没 measure 那么多,但你要是真去改过自定义 ViewGroup 的布局逻辑,就知道这里面的门道一点都不少。我个人习惯是,先把源码跑一遍,再结合项目里的坑去理解,这样记得最牢。
6.1 View 的 onLayout:为什么是空实现?
先看 View 的 onLayout 方法。你打开源码会发现,它就是个空方法:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 啥也没有
}
为什么会这样?你想想看,一个普通的 View(比如 TextView、Button),它自己是没有子 View 的。它只需要知道自己被放在哪个位置就行了,不需要去安排别人。所以 onLayout 对它来说,确实没什么可干的。
但这里有个细节要注意——空实现不代表没用。我在项目中遇到过一种情况:自定义 View 需要监听布局位置的变化。这时候你就可以重写 onLayout,在 super 调用前后做点事情。比如记录一下 oldLeft、oldTop 之类的。
6.2 ViewGroup 的 onLayout:为什么是抽象方法?
再看 ViewGroup。它的 onLayout 是这么定义的:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
注意这个 abstract 关键字。这意味着所有继承 ViewGroup 的子类,必须实现这个方法。
为什么 ViewGroup 要强制子类实现?说白了,因为只有你自己才知道你的子 View 该怎么摆。LinearLayout 是线性排列,RelativeLayout 是相对定位,FrameLayout 是叠放——这些逻辑 Android 框架没法替你决定。
我记得刚做 Android 那会儿,第一次写自定义 ViewGroup,忘了重写 onLayout,编译直接报错。当时还纳闷:怎么别的 ViewGroup 不用写?后来才明白,人家在源码里已经写好了。你自定义的,就得自己来。
6.3 layout 方法的核心逻辑:setFrame + onLayout
好,现在来看 layout 方法本身。它是整个布局流程的入口,定义在 View 中:
public void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) != 0) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
}
}
核心逻辑就两步:
- setFrame:把传进来的四个坐标值,赋值给 View 的 mLeft、mTop、mRight、mBottom。同时判断位置是否真的发生了变化。
- onLayout:如果位置变了,或者之前标记了需要重新布局,就调用 onLayout 让子 View 去排位置。
这里有个关键点:setFrame 返回的 changed 布尔值。如果 View 的位置没变(比如你传进来的坐标和原来一样),那 onLayout 就不会被调用。这是一个性能优化——避免无意义的布局计算。
我遇到过一个问题:某个自定义 ViewGroup 在滚动时频繁触发 layout,导致卡顿。后来发现是因为每次滚动都传了新的坐标,setFrame 总是返回 true,onLayout 就被反复调用。解决方案是在 onLayout 里加了个判断,只有真正需要重新排子 View 时才去计算。
6.4 流程图:layout 方法的完整调用链
为了让你更直观地理解,我画了一张流程图:
从图上可以看得很清楚:setFrame 是 gatekeeper,它决定了 onLayout 要不要执行。这个设计很巧妙——如果 View 的位置没变,就没必要触发子 View 的重新布局,省了不少计算量。
6.5 setFrame 内部做了什么?
咱们再深入一点,看看 setFrame 的源码:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mTop != top || mRight != right || mBottom != bottom) {
changed = true;
// 记录旧值
int oldLeft = mLeft, oldTop = mTop, oldRight = mRight, oldBottom = mBottom;
// 赋值新值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
// 通知背景、前景等更新
...
}
return changed;
}
逻辑其实很简单:比较新旧坐标,如果有变化就更新,并返回 true。这里有个细节——它只比较了四个边界值,没有比较宽高。因为宽高是由 right - left 和 bottom - top 计算出来的,边界变了宽高自然就变了。
我曾经踩过一个坑:在自定义 ViewGroup 里,我直接修改了子 View 的 mLeft 和 mTop,以为这样就能改变它的位置。结果发现界面根本没变。后来才意识到,必须通过 layout 方法去设置,因为 setFrame 里除了赋值,还会触发一些内部标记的更新。直接改成员变量,框架根本不知道你动了它。
6.6 总结一下
这一章的内容其实就三个点:
- View.onLayout:空实现,因为普通 View 不需要管理子 View。
- ViewGroup.onLayout:抽象方法,强制子类实现自己的布局逻辑。
- layout 方法:先调 setFrame 更新位置,再根据变化决定是否调 onLayout。
嗯,说起来简单,但实际写自定义 ViewGroup 的时候,onLayout 里的坐标计算才是最头疼的。下一章我会拿 LinearLayout 的 onLayout 源码来拆解,看看 Google 工程师是怎么处理那些边界情况的。到时候你就知道,写一个健壮的布局逻辑,要考虑多少细节了。
公众号:蓝海资料掘金营,微信deep3321