[Android开发学iOS系列] Auto Layout
内容:
- 介绍什么是Auto Layout.
- 基本使用方法
- 在代码中写约束的方法
- Auto Layout的原理
- 尺寸和优先级
- Auto Layout的使用细则
- 重要的属性
- StackView
- Layout Guide
- Performance
- Debugging
What is Auto Layout
Auto Layout会根据constraints(约束)动态计算出view hierarchy中所有View的位置和大小.
对于Android开发者来说, Auto Layout很容易上手, 它非常像ConstraintLayout
和RelativeLayout
: 给View规定它上下左右和谁对齐, 决定UI的位置和大小.
Auto Layout的约束更宽泛一些, 不仅仅是两个View之间的关系, 还有宽高, 比率等设置, 并且可以有一些大于小于等的范围设置.
Auto Layout不是一个View
开始学Auto Layout我还以为它是一个叫AutoLayout
的View, 把其他子View包进去然后设置一些放置规则, 就类似于Android的ConstraintLayout
或者RelativeLayout
.
但是其实不是, AutoLayout不是一个具体的View, 它代表的是一种计算引擎. 因为在代码里你从来不需要写AutoLayout
这个关键字, 写的从来都是Constraints
.
开发者为View设置足够多的约束, 规定和这个View位置和大小相关的因素, 这个引擎就可以为我们计算出View的位置和大小.
AutoLayout为了解决什么问题
不同屏幕适配; 可以合理应对变化的responsive UI.
改变布局有内外两种因素, 除了屏幕尺寸, 屏幕旋转, 窗口大小改变等外部因素.
内部因素还包含了内容的动态变化, 国际化的支持, 字体的调整等.
和Auto Layout平行的解决方案是什么
摆放UI有三种主要的方法:
- 在程序里给每个View设置frame.
- 设置frame, 结合使用autoresizing masks来应对外部变化. (autoresizing mask定义了一个view的frame在它的superview frame变化时应该如何变化.)
- 使用Auto Layout.
可以看出第二种只是在基于frame的方式上做出了一点改进, 所能应对的也仅仅是外部变化, 有一定的局限性. 所以可以把前两种归类为一种.
这也正是Auto Layout出现之前的解决方案, 即基于frame的布局方式.
Auto Layout的思考点不再着眼于view frame, 而是view的relationship.
如何使用Auto Layout
写iOS的UI有多种方式, Auto Layout属于UIKit, 在写的时候, 可以用storyboard, 也可以直接在代码中写约束.
在storyboard里面有一些好处, 比如所见即所得, 而且ide会给出一些warnings, 比如控件在storyboard上的位置与约束不一致, 会提示, 并且可以选择方式修复.
在storyboard里面写约束确实是不容易出错的一种方式, xcode的操作也很直观, 这里不做演示了.
之前我们也讨论过, 用storyboard写UI存在阅读性差, 代码版本管理和团队合作都有问题等.
所以具体使用需要看实际情况.
关于约束, location和size的约束不能混着用, 这个也是从逻辑上就可以理解的.
比如让某个view的top和parent的top对齐(或者再offset个常量)是可以的, 但是让top等于某个size就不能理解了.
在代码中创建约束
如果不用Interface Builder, 而是选择在代码中创建约束, 那么仍然有多种选择:
- 使用layout anchor.
- 使用
NSLayoutConstraint
类. - 使用Visual Format Language.
我们在改变约束的时候通常不会add/remove constraints, 而是active/deactivate.
使用Layout anchor
这个方法可能是最直观的一种方法.
// Get the superview's layout
let margins = view.layoutMarginsGuide
// Pin the leading edge of myView to the margin's leading edge
myView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
// Pin the trailing edge of myView to the margin's trailing edge
myView.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
// Give myView a 1:2 aspect ratio
myView.heightAnchor.constraint(equalTo: myView.widthAnchor, multiplier: 2.0).isActive = true
这里我们把每一条约束设置了isActive = true
.
也可以直接放在一个数组里一起activate, 会有性能优势:
NSLayoutConstraint.activate([
myView.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
myView.trailingAnchor.constraint(equalTo: margins.trailingAnchor),
myView.heightAnchor.constraint(equalTo: myView.widthAnchor, multiplier: 2.0)
])
使用NSLayoutConstraint
使用NSLayoutConstraint
写起来比较啰嗦, 必须给每个参数都指定值:
NSLayoutConstraint(item: myView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: myView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailingMargin, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: myView, attribute: .height, relatedBy: .equal, toItem: myView, attribute:.width, multiplier: 2.0, constant:0.0).isActive = true
这个不但写起来麻烦, 可读性也很差.
Visual Format Language (VFL)
let views = ["myView" : myView]
let formatString = "|-[myView]-|"
let constraints = NSLayoutConstraint.constraints(withVisualFormat: formatString, options: .alignAllTop, metrics: nil, views: views)
NSLayoutConstraint.activate(constraints)
用一些键盘符号来表达这个布局的. (like a way of drawing the layout you want with a series of keyboard symbols)
管道符号代表parent view的边边.
Auto Layout的工作原理
图来自于: https://developer.apple.com/videos/play/wwdc2018/220
Render loop包含如上三个阶段:
- update constraints从叶子节点向上.
- layout从parent节点向下执行.
- display即最后的绘制阶段.
这三个阶段对应的方法:
Update Constraints
它的工作是:
- 把每个公式(约束)加入计算引擎Engine里.
- 计算引擎负责解出变量: 最后的frame.
- 通知View: Superview: setNeedsLayout().
engine这里扮演一个layout cache和tracker. 收到变化时它会重新计算.
Layout
从engine得到信息后, Subview setBounds(), subview setCenter().
尺寸和优先级
了解了Auto Layout的原理之后, 看尺寸和优先级的部分就很好理解.
Intrinsic content size
有一些View有固有内容尺寸, 对于AutoLayout来说, 会默认使用intrinsic content size, 这样开发者就不用非得提供尺寸信息.
默认使用: intrinsic content size. 固有内容尺寸.
- UIImageView: image size.
- UILabel: text size.
优先级
优先级的值可以从1到1000, 默认是1000.
- Required: 1000
- Default High: 750
- Default Low: 250
有优先级是因为多个constraints之间可能会有冲突, 那么约束的要求可能不能完全100%满足, 计算引擎会在在不能满足的情况下, 尽量地减少偏差.
约束的优先级就用来表示哪条约束我们更加关心, 更想满足, 优先考虑.
优先级相关的变量
- content hugging priority: 尺寸比固有内容更大的可能性. 默认250. 值越小表示View更愿意扩张来满足约束了; 值越大表示View希望尽可能地接近固有尺寸.
- content compression resistance priority: 尺寸比固有内容尺寸更小的阻力程度. 默认750. 值越大表示这个View压缩内容的可能性越小.
Auto Layout的使用细则
Properties & Functions
有个重要的属性要提一下:
translatesAutoresizingMaskIntoConstraints
这个属性是为了兼容Auto Layout出现之前的基于frame布局的legacy layout系统, 帮助View在Auto Layout的世界里, 以legacy layout system的方式运作.
当这个属性为true, 并且设置了frame时, 引擎会自动生成constraints来满足这个frame.
这个View的属性默认为true. 当我们要用constraints时需要设置为false.
- 当在storyboard中开始为View设置constraints时, 会自动设置为false.
- 当我们在代码中给view设置约束之前, 需要自己显式地把这个属性设置为false.
如果还是用frame布局, 这个属性不用设置成false. 比如在循环里生成很多view的时候, 可能想有一些尺寸和位置用frame设置.
- sizeToFit(): 刚好包裹内容的大小.
Stack View
Stack View是在Auto Layout的基础上的, 帮助我们做一些水平或者垂直的布局, 不用写内部元素间的constraints. (类似于Android中的LinearLayout
.)
往Stack View里加需要叠放的元素用的是addArrangedSubview()
这个方法.
与此同时, addSubview()
方法可以用来加一些别的View.
几个属性:
- axis: 主轴方向.
- alignment: 对齐方式.
- distribution: 沿着主轴的分布.
Stack View是比较轻量的, 所以官方会建议尽量多使用Stack View, 只在有必要的时候写约束.
确实方便很多.
Layout Guide
很多时候为了布局的需要我们可能要包裹View或者是添加一下辅助View, 每个View都有自己的layer, 所以为了改进性能, 我们可以使用Layout Guide.
View自带一个layoutMarginsGuide.
还挺方便的. (看了这个视频: https://www.youtube.com/watch?v=4qPcMGiSADA)
Performance & Building Efficient Layouts
iOS12对AutoLayout的性能做了很多改进, 这个WWDC的talk有讲.
关于有效率的布局, 简而言之就是少做无用功.
Constraint Churn
constraint churning
是个典型的性能问题.
churn
: 搅动.
constraint churn是指更新了constraints, 但实际上view并不需要移动.
这样是给engine发送了额外的信息, 达到一定数量之后, 就会影响性能.
需要注意的是:
- 不要remove all constraints然后又add all. 可以把它们分组, 哪些是固定不变的, 那么addView的时候就加上, 然后activate; 对于需要动态变化的部分可以分两组(比如一个根据内容动态决定是否需要显示图片的例子, 可以有两个数组: imageConstraints和noImageConstraints), 单独activate/deactivate这两组约束.
- 使用isHidden可以提高效率. 比起add/remove Subview来说.
也是WWDC2018/220里提到的, 如何避免Constraint Churn:
- Avoid removing all constraints
- Add static constraints once
- Only change the constraints that need changing
- Hide views instead of removing them
Size
可以选择性地override一些尺寸, 减少text measure计算的过程:
- Return size if known without text measurement
- Use
UIView.noIntrinsicMetric
and constraints.
System Layout Size Fitting Size
intrinsic content size是view传给engine的.
而这个system layout size fitting size, 是从engine取出来的.
但是它有想不到的性能消耗. (every time you call the method, an engine is created and discarded.)
Debugging
Auto Layout中由约束引起的错误可能会有:
- 约束自相矛盾(冲突), 不能满足, 无解. (比如一个宽度即等于100又等于200, ???)
- 约束不足导致有很多可能的解. (Engine会给出一个解, 但可能不是你想要的.)
大体上是根据Log还有一些可能有帮助的view的属性和方法(供debug用).
这个视频(https://developer.apple.com/videos/play/wwdc2015/219/)的后半段有讲debug.
这里还有一个小工具网站: https://www.wtfautolayout.com/
Summary
Auto Layout是线性代数的应用实例.
有时候搬砖搬久了是不是应该慢下来欣赏一下数学的美.
References
- Understanding Auto Layout - Official Doc
- High Performance Auto Layout - WWDC2018
- Mysteries of Auto Layout, Part 1 - WWDC 2015
- Mysteries of Auto Layout, Part 2 - WWDC 2015
- Auto Layout Basics at codepath
- The Auto Layout cheat sheet
- Behind the Scenes with Auto Layout - iOS Conf SG 2019
- AutoLayout Log分析小工具