UIView Layout
iOS 中 UIKit 为 UIView 提供了这些方法来进行视图的更新与重绘:
//更新方法
- (void)setNeedsLayout;
- (void)layoutIfNeeded;
- (void)layoutSubviews;
- (CGSize)sizeThatFits:(CGSize)size
- (void)sizeToFit
//重绘方法
- (void)drawRect:(CGRect)rect;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
要理解视图的更新与重绘,那么就必然要知道 iOS 的绘图机制 The View Drawing Cycle。
The View Drawing Cycle: 在开始任何绘图操作之前,系统将一直等到当前RunLoop结束。这种延迟使您有机会使多个视图失效、从层次结构中添加或删除视图、隐藏视图、调整视图大小以及同时重新定位视图。然后,您所做的所有更改都会同时反映出来。
显然修改 UIView 的 frame、bounds 等属性后并不会马上重新绘制,而是用 RunLoop 把多次绘制聚集在一个 Cycle 里一起渲染,这显然是更加高效的行为。
layoutSubviews()
继承于 UIView 的子类重写,进行布局更新,刷新视图。 如果某个视图自身的bounds或者子视图的bounds发生改变,那么这个方法会在当前runloop结束的时候被调用。 为什么不是立即调用呢?因为渲染毕竟比较消耗性能,特别是视图层级复杂的时候。这种机制下任何UI控件布局上的变动不会立即生效,而是每次间隔一个周期,所有UI控件在布局上的变动统一生效并且在视图上更新。
为什么不是立即调用呢?
Update cycle(更新周期, Update cycle 负责布局并且重新渲染视图) 是当应用完成了你的所有事件处理代码后控制流回到主 RunLoop 时的那个时间点。正是在这个时间点上系统开始更新布局、显示和设置约束。如果你在处理事件的代码中请求修改了一个 view,那么系统就会把这个 view 标记为需要重画(redraw)。在接下来的 Update cycle 中,系统就会执行这些 view 上的更改。用户交互和布局更新间的延迟几乎不会被用户察觉到。iOS 应用一般以 60 fps 的速度展示动画,就是说每个更新周期只需要 1/60 秒。这个更新的过程很快,所以用户在和应用交互时感觉不到 UI 中的更新延迟。但是由于在处理事件和对应 view 重画间存在着一个间隔,RunLoop 中的某时刻的 view 更新可能不是你想要的那样。如果你的代码中的某些计算依赖于当下的 view 内容或者是布局,那么就有在过时 view 信息上操作的风险。
不应该在代码中显式调用这个方法。相反,有许多可以在 r及时 loop 的不同时间点触发 layoutSubviews 调用的机制,这些触发机制比直接调用 layoutSubviews 的资源消耗要小得多。
当 layoutSubviews 完成后,在 view 的所有者 view controller 上,会触发 viewDidLayoutSubviews 调用。因为 viewDidLayoutSubviews 是 view 布局更新后会被唯一可靠调用的方法,所以你应该把所有依赖于布局或者大小的代码放在 viewDidLayoutSubviews 中,而不是放在 viewDidLoad 或者 viewDidAppear 中。这是避免使用过时的布局或者位置变量的唯一方法。
layoutSubviews 调用的主要流程步骤:
- 创建
UIView子类 - 重写
layoutSubviews - 调用
setNeedsLayout - 系统调用
layoutSubviews - 执行自定义布局代码
layoutSubviews 的调用时机(会触发自动刷新触发器)
- init初始化不会触发layoutSubviews,使用initWithFrame 进行初始化时,当rect的值不为CGRectZero时,也会触发。
- addSubview会触发layoutSubviews。
- 滚动一个UIScrollView会触发layoutSubviews(layoutSubviews 会在 UIScrollView 和它的父 view 上被调用)。
- 旋转Screen会触发父UIView上的layoutSubviews事件。
- 改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
- 视图第一次加载: 当视图第一次加载到屏幕上时,系统会调用layoutSubviews来确保视图的子视图正确布局。
- 手动调用setNeedsLayout: 如果我们手动调用了视图的setNeedsLayout方法,系统将会在下一个绘制周期调用layoutSubviews。
- 更新视图的 constraints。
- 设置view的Frame会触发layoutSubviews(frame的值变化了)
如何使用 layoutSubviews
要使用 layoutSubviews,我们需要在自定义的UIView子类中重写这个方法。
- (void)layoutSubviews {
// 调用确保任何父类的布局逻辑都不会被遗漏。
[super layoutSubviews];
// 布局操作 -- 更新布局代码
}
常见的布局操作
-
调整子视图的位置和大小: 通过修改子视图的frame属性,我们可以调整它们的位置和大小,确保它们适应父视图的变化。
-
更新约束: 如果我们在项目中使用Auto Layout,那么在layoutSubviews中更新约束将是一个不错的选择,以确保布局的稳定性。
-
其他自定义布局操作: 根据具体的需求,我们还可以进行其他自定义的布局操作,以满足特定的UI设计要求。
layoutSubviews的实际应用
-
处理动态数据: 当我们的视图需要根据动态数据进行布局调整时,layoutSubviews是一个理想的位置。例如,一个聊天界面中的消息气泡高度可能会根据消息内容的不同而变化,这时我们可以在layoutSubviews中更新气泡的高度。
-
自定义动画效果: layoutSubviews的调用时机使得它非常适合用于实现一些自定义的动画效果。通过在布局调整中加入动画,我们可以创建出更加生动和流畅的用户体验。
setNeedsLayout()
标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop之内的所有布局和UI上的更新只会刷新一次,layoutSubviews一定会被调用。
layoutIfNeeded()
如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。
在当前runloop中立即刷新
layoutIfNeeded不一定会调用layoutSubviews方法。 setNeedsLayout一定会调用layoutSubviews方法(有延迟,在下一轮runloop结束前)。 如果想在当前runloop中立即刷新,调用顺序应该是:
[self setNeedsLayout];
[self layoutIfNeeded];
drawRect()
这个方法是用来重绘的。
- drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
以上1,2推荐;而3,4不提倡
drawRect方法使用注意点:
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用calayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法。
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来调用setNeedsDisplay实时刷新屏幕。
4、和 layoutSubviews 一样,你不应该直接调用 drawRect 方法,而应该通过调用触发方法,让系统在 run loop 中的不同结点自动调用。
sizeToFit()
- sizeToFit会自动调用sizeThatFits方法;
- sizeToFit不应该在子类中被重写,应该重写sizeThatFits
- sizeThatFits传入的参数是receiver当前的size,返回一个适合的size
- sizeToFit可以被手动直接调用sizeToFit和sizeThatFits方法都没有递归,对subviews也不负责,只负责自己
setNeedsDisplay()
这个方法类似于布局中的 setNeedsLayout 。它会给有内容更新的视图设置一个内部的标记,但在视图重绘之前就会返回。然后在下一个 update cycle 中,系统会遍历所有已标标记的视图,并调用它们的 drawRect 方法。如果你只想在下次更新时重绘部分视图,你可以调用 setNeedsDisplay(),并把需要重绘的矩形部分传进去(setNeedsDisplayInRect in OC)。大部分时候,在视图中更新任何 UI 组件都会把相应的视图标记为“dirty”,通过设置视图“内部更新标记”,在下一次 update cycle 中就会重绘,而不需要显式的 setNeedsDisplay 调用。然而如果你有一个属性没有绑定到 UI 组件,但需要在每次更新时重绘视图,你可以定义他的 didSet 属性,并且调用 setNeedsDisplay 来触发视图合适的更新。