最近又跟 layoutSubviews 这玩意儿较上劲了,所以想着把我这实践过程记录一下,给大伙儿也分享分享。
起初,我刚接触这东西的时候,总觉得视图布局不就那么回事嘛在初始化 `init` 或者 `viewDidLoad` 里头把子视图的 `frame` 设置好不就完事儿了?一开始我也是这么干的,简单粗暴。
但很快就碰到钉子了。比如我做一个自定义视图,里面有个按钮需要根据视图本身的宽度来居中显示。我在 `init` 里头设置,发现有时候拿到的父视图宽度不准,或者根本就是零。这就尴尬了,按钮位置肯定不对。
后来我就开始捣鼓,查资料加上自己瞎试。看到有人说 `addSubview` 会触发 `layoutSubviews`,还有改变视图大小也会。我就想,这 `layoutSubviews` 到底是个啥时候干活的?
实践过程来了:
- 我先是创建了一个简单的 `UIView`,在里面重写了 `layoutSubviews` 方法,就打印一句话,看看它啥时候被调用。
- 然后我实例化这个 `UIView`,`init` 的时候,控制台安安静静,没打印。证实了初始化确实不直接触发它。
- 我把它 `addSubview` 到另一个视图上。控制台立马打印了!说明添加子视图这操作,确实会触发父视图和子视图(如果子视图也重写了)的 `layoutSubviews`。
- 再然后,我试着去改这个视图的 `frame`,比如手动改一下它的宽度或者高度。改完之后,控制台又打印了!这说明改变视图尺寸大小也会触发。
- 还有滚动 `UIScrollView`,如果这个视图在滚动视图里面,并且因为滚动导致它的位置或者父视图的边界有变化啥的,也可能触发。这个我试了下,确实是这样,尤其是在一些复杂的嵌套滚动或者 `UITableViewCell` 复用时。
- 屏幕旋转,这个比较明显,整个界面布局都可能大变,父视图的 `layoutSubviews` 自然会被叫起来干活。
搞明白了这些触发时机,我就清楚多了。这 `layoutSubviews` 方法,感觉就像是系统给的一个“重新摆放内部小零件”的信号。当视图自己的尺寸、或者它内部的结构(比如增删子视图)发生变化后,系统觉得“欸,你这内部可能需要重新调整一下布局了”,就会来调用这个方法。
所以后来我就学乖了:
那些需要依赖视图最终尺寸和边界才能确定的布局代码,比如我前面说的那个按钮居中,我就不再放在 `init` 或者 `viewDidLoad` 里了。我把计算和设置子视图 `frame` 的代码,挪到了 `layoutSubviews` 方法里面。
但是这里头也有个坑要注意。`layoutSubviews` 可能会被调用多次!比如你稍微改下 `frame`,它就可能来一下。所以在 `layoutSubviews` 里面干的活儿不能太复杂,尽量就是纯粹的布局计算和设置 `frame`。别在里面搞什么数据请求、或者特别耗性能的计算,不然界面一调整就可能卡顿,当年我就吃过这亏,滑动一个列表卡得要死,查半天发现是在 cell 的 `layoutSubviews` 里干了不该干的事。
如果你在其他地方改了某些东西,希望视图能立刻重新布局,可以手动调用 `setNeedsLayout`。这个方法会给视图打个标记,告诉系统“我这儿需要重新布局了,下次刷新周期你看着办”。然后系统会在合适的时机去调用 `layoutSubviews`。如果想立即强制布局,就用 `layoutIfNeeded`,它会马上触发 `layoutSubviews`(如果之前被标记了需要布局的话)。这两个我实践中也经常搭配着用。
这 `layoutSubviews` 就是个专门管视图内部“摆摊”的地方,摊位大小(视图 `frame`)变了,或者要加减货架(`addSubview`/`removeFromSuperview`),它就出来重新规划一下怎么摆。理解了它的脾气,用起来就顺手多了。