上一篇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系统上加载运行图:

全文完,转载请注明出处,谢谢阅读。
————————————
