AutoLayout 相关 - wanshanhu79/Study GitHub Wiki
苹果公司在iOS6开始引入,在iOS9开始苹果公司推出了在Auto Layout基础上模仿前端Flexbox布局思路的UIStackView工具。极大地减少在约束关系设置上所做的重复操作,提升页面布局的体验。
Auto Layout依赖Cassowary算法。
还包含了布局在运行时的声明周期等一整套布局引擎系统,用来统一管理布局的创建、更新和销毁。叫做Layout Engine,是Auto Layout的核心,主导着整个界面布局。
- Layout Engine
Layout Engine会将视图、约束、优先级、固定大小通过计算转换成最终的大小和位置。Layout Engine在碰到约束变化后会重新计算布局,获取到布局后,调用superview.setNeedLayout(),然后进入 Deferred Layout Pass。 - Deferred Layout Pass 主要作用是容错处理。如果有些视图在更新约束时没有确定或缺失布局声明的话,会现在这里做容错处理。
- 接下来Auto Layout会从上到下调用layoutubviews(),通过Cassowary算法计算各个子视图的位置,算出来后将子视图的frame从Layout Engine里拷贝出来。
之后过程就跟手写布局的绘制、渲染过程一样了,就是比手写布局多了个计算过程。
Cassowary算法是以高效的界面线性方程求解算法被提出来的,它解决的是界面的线性规划问题,而线性规划问题的解法是 Simplex 算法。这个算法没有指数线性复杂度的。而Cassowary算法又是在 Simplex 算法基础上对界面关系方程进行了高效的添加、修改更新操作,不会带来时间复杂度成指数级增长的问题。
在iOS12之前,很多约束变化时都会重新创建一个计算引擎将约束关系重新加进来,然后重新计算。结果就是,涉及到的约束关系变多时,新的计算引擎需要重新计算,最终导致计算量呈指数级增加。
iOS12更多地利用了Cassowary算法的界面更新策略,使其真正完成了高效的界面线性策略计算。
intrinsicContentSize
Override this method to tell the layout system that there is something it doesn't natively understand in this view, and this is how large it intrinsically is.
固有内容大小:有些控件能通过自己显示的内容计算出需要的Size,这个自动计算出来size就叫该控件的固有内容大小。这个大小是和需要显示的内容相关的。UIButton,UILabel就是具有固有内容大小属性的控件。UIButton可以根据它的title字符串长度和需要显示的image来计算需要的Size,UILabel可以根据它的text来计算。
-(CGSize)intrinsicContentSize
就是根据它的text、attributedText和preferredMaxLayoutWidth等来计算出它的size。
当视图内容改变时,可以调用- (void)invalidateIntrinsicContentSize
方法来让AutoLayout在下次布局时重新计算。
在YYLabel的实现中,label中是调用preferredMaxLayoutWidth来处理的。
- (CGSize)intrinsicContentSize {
if (_preferredMaxLayoutWidth == 0) {
YYTextContainer *container = [_innerContainer copy];
container.size = YYTextContainerMaxSize;
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
return layout.textBoundingSize;
}
CGSize containerSize = _innerContainer.size;
if (!_verticalForm) {
containerSize.height = YYTextContainerMaxSize.height;
containerSize.width = _preferredMaxLayoutWidth;
if (containerSize.width == 0) containerSize.width = self.bounds.size.width;
} else {
containerSize.width = YYTextContainerMaxSize.width;
containerSize.height = _preferredMaxLayoutWidth;
if (containerSize.height == 0) containerSize.height = self.bounds.size.height;
}
YYTextContainer *container = [_innerContainer copy];
container.size = containerSize;
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
return layout.textBoundingSize;
}
- 只有部分视图具有intrinsicContentSize。
-
UIView没有IntrinsicContentSize;默认IntrinsicContentSize是返回(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric),UIViewNoIntrinsicMetric是UIView中定义的一个常量,值为-1,表示没有内在内容大小。当然我们也可以自定义
-
Sliders只能定义width。Sliders的height拥有IntrinsicContentSize;
-
UILabel、UIButton、UISwitch、UITextField的IntrinsicContentSize同时存在width、height;
-
UITextView、UIImageView的IntrinsicContentSize是动态变化的;
-
原理:视图的intrinsicContentSize本质上还是通过约束来实现的,AutoLayout在每个坐标轴方向设置两个约束,分别是:contentHugging(内容吸附,默认250)、compressionResistance(压缩阻力,默认750)。
当使用Interface Builder布局时,默认会把UIImageView、UILabel的contenthugging优先级调整为251。Apple这么做原因应该是:大部分情况下,当与UITextField等共存,当需要拉伸时,一般是希望拉伸后者。
用来约束换行操作,当内容超过约束区域时就会自动换行,并且更新约束布局,preferredMaxLayoutWidth 就是告诉最大的参考宽度
// Support for constraint-based layout (auto layout)
// If nonzero, this is used when determining -intrinsicContentSize for multiline labels
@property(nonatomic) CGFloat preferredMaxLayoutWidth NS_AVAILABLE_IOS(6_0);
多线标签的首选最大高度,在使用systemLayoutSizeFittingSize计算动态高度时,如果是计算多行UILabel,并且width不是固定的话(自动布局适配屏幕就是不固定,写死的数值算固定),需要设置label的preferredMaxLayoutWidth属性,否则计算的结果会有偏差。
preferredMaxLayoutWidth未设置值时,会调用intrinsicContentSize多次,待计算好preferredMaxLayoutWidth后,才能返回准确的intrinsicContentSize
根据当前的约束返回视图的最优大小 targetSize:获得一个视图尽可能小,指定UILayoutFittingCompressedSize不变。获得尽可能大的一个视图,指定UILayoutFittingExpandedSize不变。
值得一提的是,在使用[view systemLayoutSizeFittingSize:]时,要注意尽量确保view的constraints的完整性,这样参数UILayoutFittingCompressedSize和UILayoutFittingExpandedSize得到的结果是一样的。
- (CGSize)sizeThatFits:(CGSize)size; // return 'best' size to fit given size. does not actually resize view. Default is return existing view size
- (void)sizeToFit; // calls sizeThatFits: with current view bounds and changes bounds size.
调用sizeThatFits:并不改变view的size,它只是让view根据已有content和给定size计算出最合适的view.size。
简单来说,sizeToFit等价于:
// calls sizeThatFits
CGSize size = [self sizeThatFits:self.bounds.size];
// change bounds size
CGRect bounds = self.bounds;
bounds.size.width = size.width;
bounds.size.height = size.width;
self.bounds = bounds;
针对frame布局
该优先级表示一个控件抗被拉伸的优先级。优先级越高,越不容易被拉伸,默认是251.
该优先级和上面那个优先级相对应,表示一个控件抗压缩的优先级。优先级越高,越不容易被压缩,默认是750.
深入理解Auto Layou 自动布局 Auto Layout (原理篇)
- IntrinsicContentSize是Auto Layout的输入源,而FittingSize是Auto Layout的输出结果。IntrinsicContentSize是转化为约束集合的一部分,参与Auto Layout的布局计算。而一个视图的FittingSize是基于子视图的约束集合、内容、来计算出视图本身的尺寸。
- UIStackView看起来好像是具有IntrinsicContentSize,实际上并不是,它是基于FittingSize。