写完了Part 1
就被接踵而至的新项目和新版本忙的不可开交,转眼间一个季度就已经过去了,而这篇Part 2
却迟迟还没有出现。实在是抱歉没有及时更新。不过有一个好消息就是FDStackView
已经被使用在我们自己的项目中,并且我们的项目也已经经过了两个版本的迭代,FDStackView
可以说还是相当稳定的,并且可以顺利的通过苹果的审核机制,对这方面有顾虑的小伙伴们可以放心大胆的使用了。同时我们也将它的版本号从1.0-alpha
升级到1.0
。在此感谢一下各位热心的小伙伴们在Github
上提出的issue
,以及着重感谢下@里脊串对FDStackView
的重度使用及提出的各种隐晦的bug
。后续我们将会对性能的优化做出改进,以及对Layout Margins
的支持。
回到主题,这篇文章主要介绍StackView
的实现,即如何通过现有AutoLayout
技术实现StackView
这样的一个控件。这里说明一下,当初我们编写FDStackView
的时候,UIStackView
还没有支持Layout Margins
,所以我们也没有添加Layout Margins
的支持,不过目前的iOS SDK
已经增加了这一部分的支持,所以在打开layoutMarginsRelativeArrangement
属性的情况下,StackView
创建出的约束会与我后面所介绍的内容有一些出入,不过问题不大,仅仅是部分约束的firstItem
由StackView
本身变成UILayoutGuide
的区别。
实现StackView
主要包括这几个技术点:
alignment
和distribution
的约束如何添加和管理;spacing
和distribution
的关系及约束的创建;- 子视图的隐藏显示如何处理;
- 子视图的
intrinsicContentSize
发生变化时如何处理。
我们对
UIStackView
进行了详细的研究,包括dump
出所有UIStackView
的相关私有类,各个类的方法,实例变量等。还需要添加符号断点来跟踪各个方法的调用顺序及各个实例变量的值得变化情况。同时还需要分析各个状态下UIStackView
的约束constraints
的情况,包括约束的个数,连接的方式,及约束所添加到的视图等。经过以上的各种分析之后,我们又通过在IB
中借助UIView
手动连接约束的方式,连出每一个UIStackView
所对应的状态。经过这一番调查与研究我们已经大概摸清的UIStackView
的工作原理与实现方式。
如上篇文章所说,在进行了详尽的研究之后,总结出大概需要攻克的是这几个技术点,以尽可能的与UIStackView
的实现保持一致,在难以完成的地方通过自己的方式实现。在这之前先介绍一下我们使用到的几个私有类。
CATransformLayer
StackView
是一个透明不可见的容器,主要就是因为这个layer
,我们继承了它并重载了两个方法,setOpaque:
和setOpaque:
,用于避免产生警告⚠️。也就是项目中的FDTransformLayer
。
_UILayoutSpacer
这是一个私有类,它的主要作用是用了辅助StackView
创建alignment
方向上的约束,它的父类是UILayoutGuide
,并不是一个UIView的子类,所以我们并不能以熟悉的方式对它添加约束。但是在知道了它的作用之后,我们完全可以使用一个UIView
来代替它,同时它也是不可见的,所以它的layer
自然也是FDTransformLayer
。这是项目中的FDLayoutSpacer
。
_UIOLAGapGuide
与_UILayoutSpacer
相同是UILayoutGuide
的子类,用来辅助distribution
方向上的约束创建,并且只有UIStackViewDistributionEqualSpacing
和UIStackViewDistributionEqualCentering
两种模式下它才会出现。在项目中我们通过UIView
的子类FDGapLayoutGuide
来实现它。
_UILayoutArrangement
同样是一个私有类,用来管理StackView
及其子视图的约束的创建。它是一个父类,在FDStackView
中我们使用FDStackViewLayoutArrangement
来与之对应。
_UIAlignedLayoutArrangement
该类是_UILayoutArrangement
的子类,用来控制alignment
方向上的约束的创建及管理,它维护了一个_UILayoutSpacer
并负责它的生命周期。在FDStackView
中我们以更直接的FDStackViewAlignmentLayoutArrangement
来对它命名。
_UIOrderedLayoutArrangement
与_UIAlignedLayoutArrangement
相对,用来控制distribution
方向上的约束创建及管理,它维护了一组_UIOLAGapGuide
。在FDStackView
中我们以更直接的FDStackViewDistributionLayoutArrangement
来对它命名。
先提前解释几个后面会提到的名词:
canvas
:canvas
是什么?翻译过来是画布的意思,其实就是容器也就是StackView
本身Ambiguity Suppression
:经常Debug``AutoLayout
的同学可能对这个词并木陌生,一般约束产生冲突或者模棱两可的时候,控制台就会输出一组信息,其中就会包含这个词。这里就是抵制模棱两可的约束的意思。StackView
中会创建一些低优先级的约束来完成这件事儿,以防止控制台打出AutoLayout
异常的log
。minAttribute
:是NSLayoutAttribute
一个便捷获取方式,针对不同的axis
会对应不同的NSLayoutAttribute
,可能是NSLayoutAttributeTop
也可能是NSLayoutAttributeLeading
。centerAttribute
:同样针对不同的axis
可能是NSLayoutAttributeCenterY
或者NSLayoutAttributeCenterX
。maxAttribute
:同样针对不同的axis
可能是NSLayoutAttributeBottom
或者NSLayoutAttributeTrailing
。dimensionAttribute
:同样针对不同的axis
可能是NSLayoutAttributeHeight
或者NSLayoutAttributeWidth
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
此外UIStackView
的约束的管理方式也十分的奇妙。除了一个例外的Ambiguity Suppression
的约束,其余不管约束何种关系的约束都是add在canvas
上的。既然约束都加在了canvas
上,那这么多的约束如何区分何管理呢?
这里有个小技巧,那就是用weakToWeak
的NSMapTable
来管理,key
是约束的firstItem
,value
是约束,而且因为NSMapTable
是weakToWeak
的,所以key
和value
所对应的object
并不会增加引用计数,不会带来内存上的管理困难。若要找一个view
所关联约束,直接取view
作为key
的value
就可以了。_UILayoutArrangement
维护了多个这样的NSMapTable
,分别来管理不同作用的约束。不得不说这样的设计真的是太巧妙了。
alignment
和distribution
的约束如何添加和管理
先给一张图看一下什么是alignment
和distribution
以及Spacing
:
在介绍实现之前,我先介绍一下StackView
的各种alignment
模式都是什么效果的:
- UIStackViewAlignmentFill:这种就是填充满整个
StackView
了,用得比较多。
- UIStackViewAlignmentLeading:这种是左对齐。
- UIStackViewAlignmentTop:这种是上部对齐。
- UIStackViewAlignmentFirstBaseline:这种是让
arrangedSubviews
按照firstBaseline
对齐。只能出现在水平的StackView
中。
- UIStackViewAlignmentCenter:这种是居中对齐。
- UIStackViewAlignmentTrailing:这种是右部对齐。
- UIStackViewAlignmentBottom:这种是底部对齐。
- UIStackViewAlignmentLastBaseline:这种是让
arrangedSubviews
按照lastBaseline
对齐。同样只能出现在水平的StackView
中。
下面介绍实现,首先是alignment
方向,alignment
方向的约束主要包括4种
1 2 3 4 5 6 7 8 9 |
|
- canvasConnectionConstraints:它管理的是
arrangedSubviews
与canvas
之间的约束; - hiddingDimensionConstraints:它管理的是当
arrangedSubviews
有hidden
的时候,该arrangedSubview
的有关dimensionAttribute
的约束; - systemConstraints:它是由
_UILayoutSpacer
来管理的,它管理了spacer与arrangedSubviews
之间的约束,因为这些约束的firstItem
都是spacer自身,所以就不需要使用NSMapTable
而直接是NSArray
。另外spacer只有在alignment
不是UIStackViewAlignmentFill
的时候才会被创建,所以当alignment
是UIStackViewAlignmentFill
时,是没有systemConstraints的
; - alignmentConstraints:它管理的是
arrangedSubviews
之间的约束,它包括两组NSMapTable
,根据alignment
的不同具体的约束也不同,具体的NSMapTable
的key
与alignment
及axis
的关系如下表:
可以看到除了UIStackViewAlignmentFill
模式以外,都会有一个Ambiguity Suppression
的key,这个key对应的NSMapTable
的就管理了前面提到的那些低优先级防止布局时出现模棱两可状态的约束。此外Baseline
相关的约束是只有在axis
为Horizontal
时才会有的,并且UIStackViewAlignmentFirstBaseline
和UIStackViewAlignmentTop
,UIStackViewAlignmentLastBaseline
和UIStackViewAlignmentBottom
的key值是相同的。
这个key的名字之所以这么取也是有讲究的,它代表着它所对应的NSMapTable
管理的约束关系。举个例子:axis
为Horizontal
,alignment
为UIStackViewAlignmentFill
时,key为Top
和Bottom
,那么Top
对应的NSMapTable
管理的约束就是arrangedSubviews
之间NSLayoutAttributeTop
相等的约束。同理Bottom
就是NSLayoutAttributeBottom
相等的约束。
这样结合alignment
的效果来看就很容易理解,UIStackViewAlignmentFill
模式需要arrangedSubviews
都充满容器,那么自然他们的NSLayoutAttributeTop
和NSLayoutAttributeBottom
需要都相等,而UIStackViewAlignmentTop
模式需要top
对齐那么只需要NSLayoutAttributeTop
相等就OK了。
这里还有一个点就是arrangedSubviews
之间的约束不是迭代添加的,而是都与第一个arrangedSubview
创建关系。假设有3个view
,那就是view2
与view1
建立约束,view3
同样与view1
建立约束而不是与view2
迭代建立约束。
这4种约束的创建顺序是:
FDLayoutSpacer的systemConstraints
canvasConnectionConstraints
alignmentConstraints
hiddingDimensionConstraints
FDLayoutSpacer的systemConstraints
在FDStackViewAlignmentLayoutArrangement
中被称为spanningLayoutGuideConstraints
,创建方法是
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 |
|
首先判断一些不需要创建或者不需要更新这组约束的情况,比如之前提到的alignment
为UIStackViewAlignmentFill
或者没有arrangedSubview
的时候。接下来创建一个宽或高为0
的约束给spacer,因为对于后面添加的约束而言,spacer是缺少这样的一个约束以保证它能够正确布局。最后就是把每一个arrangedSubview
与spacer分别建立minAttribute
和maxAttribute
的约束,这些约束的constant
都是0
,但是关系却不一定都是等于,需要根据alignment
的属性不同来动态调整,有可能是大于等于,也有可能是小于等于。这需要查表来得到。
下一步创建canvasConnectionConstraints
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 |
|
因为这是alignment
的canvasConnectionConstraints
,所以只需关注它自己的minAttribute
和maxAttribute
两个方向与canvas
的约束即可,其余两个方向会在distributionLayoutArrangement
中创建。
特别的是如果alignment
是UIStackViewAlignmentCenter
的话需要加上一个centerAttribute
的约束。如果是alignment
是baseline
相关的话还要给canvas
添加一个高为0
的低优先级约束,用来满足某些特殊情况下canvas
约束不满足的情况。
具体与canvas
建立约束关系的firstItem
及relation
关系是根据alignment
类型以及NSLayoutAttribute
的不同而不同的,情况比较多我就不一一列举了,同样是根据查表得到,具体可以看代码去查。
最后是alignmentConstraints
和hiddingDimensionConstraints
,虽然前面说它们两个的顺序是一前一后创建,但其实并不是,它们可以说是一起创建的,首先取出第一个arrangedSubview
作为guardView
,然后循环遍历其余arrangedSubview
,先添加alignmentConstraint
,如果这个arrangedSubview
是hidden
的那么就会再添加一个hiddingDimensionConstraint
。
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 |
|
这里的alignmentConstraint
的创建都是guardView
与其余的arrangedSubview
创建relation
关系为相等的约束,而NSLayoutAttribute
的选择仍然是查表法,根据axis
和alignment
的不同而选择不同的NSLayoutAttribute
。
如果alignment
不是UIStackViewAlignmentFill
模式的话,就会给arrangedSubview
创建一个dimensionAttribute
为0
的低优先级约束,称为ambiguitySuppressionConstraint
放在上图中key
为Ambiguity Suppression
的NSMapTable
中。
现在解释一下本文章Part 1
中最后提到的UIStackView
当alignment
为UIStackViewAlignmentFill
时,最高视图隐藏掉,而其余视图没有变成第二个的视图的高度的bug
。原因就是在UIStackView
的中实现中AlignmentLayoutArrangement
是没有管理hiddingDimensionConstraints
的,所以当视图被隐藏了后,那个视图被添加了一个宽为0
的约束,视觉上看不到了,但是高方向的约束仍然存在,所以仍然会撑开StackView
,所以在FDStackView
中我们在alignment
方向上同时增加了hiddingDimensionConstraints
,视图被hidden
后,会在高度方向上也给他加上一个高0
为的约束,而且这个优先级也很有讲究需要跟它的contentCompressionResistancePriority
设为一样,这样才不会在AutoLayout
布局系统中当用户人为添加一个高度约束后产生冲突。
写了这么多,才写完第一个技术点的第一部分,内容确实比较多,我写的也比较乱,时间比较紧所以写作时间是间断的,所以思维也是间断跳跃的,还麻烦各位看官多多包涵。本来打算一篇写完的,但是这么长,还是有必要在分一下的,Part 2
就到这吧,其余的内容就在Part 3
吧。
————————————