上一篇Part 2
只介绍了第一个技术点alignment
和distribution
的约束如何添加和管理的alignment
这一部分的内容,这一篇继续介绍distribution
的约束添加和管理。
同样的在介绍实现之前,我先介绍一下StackView
的各种distribution
模式都是什么效果的:
- UIStackViewDistributionFill:这种应该是目前最常用的了,它就是将
arrangedSubviews
填充满整个StackView
,如果设置了spacing,那么这些arrangedSubviews
之间的间距就是spacing。如果减去所有的spacing,所有的arrangedSubview
的固有尺寸(intrinsicContentSize
)不能填满或者超出StackView
的尺寸,那就会按照Hugging
或者CompressionResistance
的优先级来拉伸或压缩一些arrangedSubview
。如果出现优先级相同的情况,就按排列顺序来拉伸或压缩。
- UIStackViewDistributionFillEqually:这种就是
StackView
的尺寸减去所有的spacing之后均分给arrangedSubviews
,每个arrangedSubview
的尺寸是相同的。
- UIStackViewDistributionFillProportionally:这种跟FillEqually差不多,只不过这个不是讲尺寸均分给
arrangedSubviews
,而是根据arrangedSubviews
的intrinsicContentSize
按比例分配。
- UIStackViewDistributionEqualSpacing:这种是使
arrangedSubview
之间的spacing相等,但是这个spacing是有可能大于StackView
所设置的spacing,但是绝对不会小于。这个类型的布局可以这样理解,先按所有的arrangedSubview
的intrinsicContentSize
布局,然后余下的空间均分为spacing,如果大约StackView
设置的spacing那这样就OK了,如果小于就按照StackView
设置的spacing,然后按照CompressionResistance
的优先级来压缩一个arrangedSubview
。
- UIStackViewDistributionEqualCentering:这种是使
arrangedSubview
的中心点之间的距离相等,这样没两个arrangedSubview
之间的spacing就有可能不是相等的,但是这个spacing仍然是大于等于StackView
设置的spacing的,不会是小于。这个类型布局仍然是如果StackView
有多余的空间会均分给arrangedSubviews
之间的spacing,如果空间不够那就按照CompressionResistance
的优先级压缩arrangedSubview
。
在介绍distribution
的约束创建和管理的过程中也涉及到了第二个知识点spacing
和distribution
的关系及约束的创建的内容,所以这两部都在这里介绍了。
distribution
方向同样也包括4种约束,这4种约束也都是添加到canvas
上的,除此之外它还包括一组通过NSMapTable
维护的FDGapLayoutGuide
。
1 2 3 4 5 6 7 8 |
|
- canvasConnectionConstraints:它管路的是
arrangedSubviews
与canvas
之间的约束; - edgeToEdgeConstraints:它管理的是
arrangedSubviews
之间一个接一个的约束,这里需要注意这些约束的常量是StackView
的spacing,但是关系却不一定是相等。还有就是如果有个arrangedSubview
被hidden
了那么它仍然参与到edgeToEdge
的约束创建及布局中,只不过是把它与后一个arrangedSubview
之间的edgeToEdgeConstraint
的常量由spacing设置为0
。 - relatedDimensionConstraints:它管理的是
arrangedSubviews
之间distribution
各种相等关系的约束,这里面的管理的约束是StackView
的distribution
布局的精髓所在。如果是UIStackViewDistributionFill
模式的话,是没有relatedDimensionConstraint
的。UIStackViewDistributionFillEqually
与UIStackViewDistributionFillProportionally
使用的是一种类型的约束,而UIStackViewDistributionEqualCentering
与UIStackViewDistributionEqualSpacing
使用的却是另一种类型的约束,后面在详细介绍。 - hiddingDimensionConstraints:它管理的是当
arrangedSubviews
有hidden
的时候,该arrangedSubview
的有关dimensionAttribute
的约束; - spacingOrCenteringGuides:这个管理的就不是约束了,它是一组
FDGapLayoutGuide
,只用在UIStackViewDistributionEqualCentering
和UIStackViewDistributionEqualSpacing
这两种模式中,FDGapLayoutGuide
用来连接左右两个arrangedSubView
,作为一个辅助view来约束左右两个view的位置关系。spacingOrCenteringGuides
的key是FDGapLayoutGuide
连接的左边的arrangedSubview
。
最后说明的就是FDGapLayoutGuide
与arrangedSubView
相连接的约束没有被NSMapTable
所管理,它们就只是被加到了canvas
上。因为当模式改变时,所有的FDGapLayoutGuide
会被移除或者重建,所以跟它们相关的约束也会被一并清楚。
那么以上几种约束的创建顺序是怎样的呢?
- 首先是
canvasConnectionConstraints
; - 其次是每一种模式都会涉及到的
edgeToEdgeConstraints
; - 然后再遍历所有
arrangedSubviews
,如果有arrangedSubview
被hidden
了,那么就会创建hiddingDimensionConstraints
; - 最后是
relatedDimensionConstraints
,这里如果是UIStackViewDistributionEqualCentering
和UIStackViewDistributionEqualSpacing
这两种模式的话,会先创建出spacingOrCenteringGuides
。
下面具体来看,首先canvasConnectionConstraints
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
比较简单,先判断一下不需要创建的情况,然后就是根据axis
选用不同的NSLayoutAttribute
,将第一个和最后一个arrangedSubview
分别与StackView
创建相等的约束。这样一来再加上FDStackViewAlignmentLayoutArrangement
中创建的两个canvasConnectionConstraints
,整个canvas
的上下左右四个方向的约束就都有了,满足了canvas
布局的基本条件。
接下来是edgeToEdgeConstraints
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
先移去旧的相关约束,然后将arrangedSubviews
依次迭代遍历,根据axis
选择正确的NSLayoutAttribute
创建首尾相接的约束,常量为StackView
的spacing,关系则根据distribution
的不同而或等于或大于等于。
这里如前面介绍的一样,如果这个arrangedSubview
是hidden
的那么它仍然参与edgeToEdgeConstraints
的创建,只不过它与后一个arrangedSubview
的约束常量不再是spacing而是0
。还有一个特殊的就是如果是最后一个arrangedSubview
被hidden
了,那么它与前一个arrangedSubview
的约束的常量也同样是0
。
最后再遍历所有arrangedSubviews
,如果有arrangedSubview
被hidden
了,那就根据axis
给这个arrangedSubview
创建一个常量为0
的dimensionConstraint
。
如果是UIStackViewDistributionFill
的话,那么到这里所有distribution
的约束就已经创建完了,已经满足需求了。但是其他几种还要有后续的步骤。
先来看UIStackViewDistributionFillEqually
和UIStackViewDistributionFillProportionally
这两种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
仍然是先干掉旧的约束,然后跟前面不同的是要取出所有的非hidden的arrangedSubview
添加约束,而不是所有arrangedSubview
。
这两个distribution
类型是将当前axis
所对应的dimensionAttribute
的约束作用在arrangedSubviews
上,如果是UIStackViewDistributionFillEqually
,那么约束的比例(multiplier
)就是1
,如果是UIStackViewDistributionFillProportionally
,那multiplier
就需要通过计算得出,是通过两个arrangedSubview
的intrinsicContentSize
做比值,这样就能保证arrangedSubview
最终会按照intrinsicContentSize
的比例来分配StackView
的空间布局。
再来看UIStackViewDistributionEqualCentering
和UIStackViewDistributionEqualSpacing
这两种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
先创建spacingOrCenteringGuides
,开始是干掉旧的spacingOrCenteringGuides
。这里使用的仍然是visiableItems。
FDGapLayoutGuide
用来连接左右相连的两个可见arrangedSubview
。
这两个distribution
不同的地方就是UIStackViewDistributionEqualSpacing
的FDGapLayoutGuide
连接的是arrangedSubview
的minAttribute
和maxAttribute
,而UIStackViewDistributionEqualCentering
的FDGapLayoutGuide
连接的却是arrangedSubview
的centerAttribute
。
接下来就是创建relatedDimensionConstraints
,就是根据axis
不同给对应的dimensionAttribute
创建相等的约束即可,这些约束是作用在FDGapLayoutGuide
上的,而与前面那两种distribution
类型不同。这就是一开始说的relatedDimensionConstraints
中的两种类型的约束。
到此整个distribution
方向的约束也都创建完了。加上alignment
方向创建的约束,StackView
已经可以使用了。
介绍完这些再回过头来看本文章Part 1
中后面提到的UIStackView
的第一个bug,当存在spacing的时候UIStackViewDistributionFillProportionally
这个类型的StackView
是烂掉的。我刚才看了一下,苹果仍然没有修复这个bug。
具体的原因那篇文章中已经解释了,现在说下为什么FDStackView
没有这个问题,相信看完前面创建约束的过程,读者朋友应该就能发现我们并没有像UIStackView
那样将canvas
的dimensionAttribute
乘以一个系数作为arrangedSubview
的dimensionConstraint
。我们的arrangedSubview
的dimensionConstraint
是与canvas
无关的,是arrangedSubviews
之间的比例关系,而且spacing在之前的edgeToEdgeConstraints
中就已经创建了,这两者是分开创建的,所以算法不同,自然也就不会出现这个bug。
下面看其余的知识点:
子视图的隐藏显示如何处理
如果一个已经布好局的StackView
,在一个arrangedSubview
被hidden
或者show
之后,那么其余的arrangedSubviews
也要做出相应变化,来相应这种变化。
在FDStackView
这里我们是通过KVO
监测每一个arrangedSubview
的hidden
属性,当任何一个arrangedSubview
属性发生变化后,我们就通过rebuild
的方式重新创建整个StackView
的约束,就是重新布局一遍。这是目前1.0
版本的处理方式,这样势必会带来性能的损失,这也是我们后续优化性能的关键。
子视图的intrinsicContentSize
发生变化时如何处理
什么叫子视图的intrinsicContentSize
发生变化呢?举个例子,一个已经布好局的StackView
,其中有一个arrangedSubview
是一个UILabel
,但是这个UILabel
被重新setText
了,那么它的intrinsicContentSize
就会发生变化,自然StackView
的布局如果不发生变化的话就是错误的。所以在这种情况下StackView
也要做出处理。
这里我们研究了UIStackView
的实现方式,一个arrangedSubview
的intrinsicContentSize
发生变化如何被捕捉到,是我们未知的,UIKit
并没有暴露任何方法给我们,我们只能通过下符号断点的方式给dump出来的UIStackView
的私有类。
研究发现当一个arrangedSubview
的intrinsicContentSize
发生变化时,UIStackView
总会调用到_intrinsicContentSizeInvalidatedForChildView:
这个私有方法,参数为发生变化的arrangedSubview
。所以我们就把这一私有方法给替换了,借助UIKit
内部的机制来帮我们通知一个arrangedSubview
的intrinsicContentSize
发生变化的这种情况。
1 2 3 4 5 6 7 8 9 |
|
接到这种通知之后,我们目前也是通过rebuild
的方式来重建StackView
的约束的。其实对于这种情况以及上面提到的hidden
的情况,我们都能得到具体发生变化的那个arrangedSubview
,这也将会是后续优化的突破口。
到此整个FDStackView
的设计实现过程都介绍完了,当然还有一些零零碎碎的点没有说,都在源码里了。后续版本会增加Layout Margins
的支持,以及性能优化。
最后在附一张UIStackView
及FDStackView
在不同iOS
系统上加载运行图:
全文完,转载请注明出处,谢谢阅读。
————————————