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 之类的。

小提示: 如果你重写了 View 的 onLayout,记得一定要调用 super.onLayout()。虽然它是空实现,但保不齐后续 Android 版本在里面加了什么逻辑。兼容性这种事,宁可信其有。

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 不用写?后来才明白,人家在源码里已经写好了。你自定义的,就得自己来。

注意: 如果你继承的是 ViewGroup 的子类(比如 LinearLayout),那 onLayout 已经被实现了,你不需要再重写。只有直接继承 ViewGroup 时,才必须实现 onLayout。

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;
    }
}

核心逻辑就两步:

  1. setFrame:把传进来的四个坐标值,赋值给 View 的 mLeft、mTop、mRight、mBottom。同时判断位置是否真的发生了变化。
  2. onLayout:如果位置变了,或者之前标记了需要重新布局,就调用 onLayout 让子 View 去排位置。

这里有个关键点:setFrame 返回的 changed 布尔值。如果 View 的位置没变(比如你传进来的坐标和原来一样),那 onLayout 就不会被调用。这是一个性能优化——避免无意义的布局计算。

我遇到过一个问题:某个自定义 ViewGroup 在滚动时频繁触发 layout,导致卡顿。后来发现是因为每次滚动都传了新的坐标,setFrame 总是返回 true,onLayout 就被反复调用。解决方案是在 onLayout 里加了个判断,只有真正需要重新排子 View 时才去计算。

6.4 流程图:layout 方法的完整调用链

为了让你更直观地理解,我画了一张流程图:

layout 方法调用流程 layout(l, t, r, b) setFrame(l, t, r, b) changed? onLayout(changed, l, t, r, b) 结束 直接结束

从图上可以看得很清楚: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 里除了赋值,还会触发一些内部标记的更新。直接改成员变量,框架根本不知道你动了它。

核心结论: 永远不要直接修改 View 的 mLeft、mTop 等成员变量。想改变 View 的位置,请调用 layout() 方法。这是唯一正确的途径。

6.6 总结一下

这一章的内容其实就三个点:

  • View.onLayout:空实现,因为普通 View 不需要管理子 View。
  • ViewGroup.onLayout:抽象方法,强制子类实现自己的布局逻辑。
  • layout 方法:先调 setFrame 更新位置,再根据变化决定是否调 onLayout。

嗯,说起来简单,但实际写自定义 ViewGroup 的时候,onLayout 里的坐标计算才是最头疼的。下一章我会拿 LinearLayout 的 onLayout 源码来拆解,看看 Google 工程师是怎么处理那些边界情况的。到时候你就知道,写一个健壮的布局逻辑,要考虑多少细节了。


公众号:蓝海资料掘金营,微信deep3321