StackView们
- UIStackView是iOS9新引入的控件。它支持垂直和水平排列多个子视图(SubView)。如:水平放置三个按钮,等宽,并且按钮间的间隙为10。如果自己实现的话,会分麻烦(无论计算frame还是设置约束,都需要很多代买),而 UIStackView很容易实现。当然还有很多其他强大功能。如,隐藏UIstackView中的某个视图后,其他视图的位置自动计算并变化。完全不用自己去实现约束值。省时省力。可是只支持iOS9+,我想在iOS7/8中使用怎么办?下面两个第三方库,支持iOS7/8
- OAStackView,基于OC的StackView库,支持iOS7+以上的系统。同时支持代码和IB视图。功能强大,无需质疑。
- TZStackView,基于Swift的StackView库,同样支持iOS7+以上的系统,但是不支持storyboard。
教程
raywenderlic中关于UIStackView的教程在这里,简单易懂,通过这个教程可以了解UIStackView的基本特性和使用方法。
对于TZStackView和OAStackView,官方事例就是最好的教程了。
OAStackView关于子视图等分和添加间隙功能的实现简析
官方示例:
在viewDidLoad中添加如下代码:
UILabel *l1 = [[UILabel alloc] init];
l1.text = @"Label 1";
UILabel *l2 = [[UILabel alloc] init];
l2.text = @"Label 2";
OAStackView *stackView = [[OAStackView alloc] initWithArrangedSubviews:@[l1, l2]];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:stackView];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-30-[stackView]"
options:0
metrics:0
views:NSDictionaryOfVariableBindings(stackView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[stackView]"
options:0
metrics:0
views:NSDictionaryOfVariableBindings(stackView)]];
很方便的实现了,两个按钮的垂直排列。默认是垂直排列的。如果想要水平排列,修改stackView.axis值为UILayoutConstraintAxisHorizontal即可。这里要注意,因为不是用IB创建的View,所以要设定View的translatesAutoresizingMaskIntoConstraints属性为NO,否则排列属性不生效。当非IB创建时,属性默认为YES;当IB创建View时,属性默认为NO。
原理
那么是如何实现的呢?感到非常好奇。之前的想法是在subView之间添加空白视图实现等宽间隔排列。不过OAStackView不是这样实现的。OAStackView是添加subViews之间的约束设置他们之间的间距为0,然后再将View与subView的约束设置为0,来实现初始化布局。然后再设定distribution属性值为:OAStackViewDistributionFillEqually,设定几个view的(Equal Width、 Equal Height)等width/height约束,来实现等宽分布。下面来具体分析,等等,先来介绍下VFL。
VFL
啥是VFL?VFL(Virsual Format Language)是一种虚拟的格式化语言,主要用来创建AutoLayout的约束字符串。上面constraintsWithVisualFormat的第一个参数就是VFL创建的。
示例,如:V: |-(0)-Label1-(0)-Label2-(0)-|
方向:从左到右,从上到下
V:表示方向为垂直方向,也就是竖向;H为横向。
|:竖线表示为边界(当前所在View的边界),这里紧邻方向表示符V,方向是从上到下,因此表示上面界。
0:NSNumber 0 表示约束值为0。这里是Label1距离上边界的约束为0。
Label1:表示对象Label1。
0:表示Label1和Label2的约束为0.
Label2:表示对象Label2。
0:表示Label2和下边界的约束为0.
|:表示下边界。
想要继续了解的,请移步这里.
分析
继续刚刚的话题,上面代码片段创建的两个Label之间的约束关系,用VFL表示,见下方:
垂直方向,两个Label距离彼此和边界的距离(space)为0,用VFL标定垂直方向的约束,则实现为:
V: |-(0)-Label1-(0)-Label2-(0)-|
水平方向,两个Label分别距离边界距离(space)为0,水平方向约束,实现为:
H: |-(0)-Label1-(0)-|
H: |-(0)-Label2-(0)-|
这样就标定了Label在StackView中的约束(4个方向:上下左右),从而达到了排列的目的。
设定distribution属性值为OAStackViewDistributionFillEqually后的约束呢?这里设定axis值为UILayoutConstraintAxisHorizontal,水平排列。效果图:
VFL表示:
水平方向:H: |-0-(Label 1)-0-(Label 2-3-4-5-6)-0-|
垂直方向:V: |-(0)-(Label 1)-(0)-|
和 H: |-(0)-(Label 2-3-4-5-6)-(0)-|
其他(等宽):label1.width == label2-3-4-5-6.width
这里多了一个等宽的约束。其实用IB实现等宽分布视图的方式也是这样设定的。最后会有storyboard中连线添加约束的截图。
viewDidLoad中添加如下代码:
UILabel *l1 = [[UILabel alloc] init];
l1.text = @"Label 1";
l1.backgroundColor = [UIColor orangeColor];
UILabel *l2 = [[UILabel alloc] init];
l2.text = @"Label 2-3-4-5-6";
l2.backgroundColor = [UIColor lightGrayColor];
OAStackView *stackView = [[OAStackView alloc] initWithArrangedSubviews:@[l1, l2]];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
NSLog(@"stackView Constraints: %@",stackView.constraints);
stackView.axis = UILayoutConstraintAxisHorizontal;
stackView.distribution = OAStackViewDistributionFillEqually;
[self.view addSubview:stackView];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-30-[stackView]"
options:0
metrics:0
views:NSDictionaryOfVariableBindings(stackView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[stackView]"
options:0
metrics:0
views:NSDictionaryOfVariableBindings(stackView)]];
看看Log中输出的内容,约束刚刚好是上面初始化后两个Label的约束形式。
"",
"",
"",
"",
"",
"",
""
让我们继续,在stackView.distribution = OAStackViewDistributionFillEqually;
设置断点:
运行程序,点击下一步
setDistribution:(OAStackViewDistribution)distribution
方法中了。
这时我们在控制台中打印下stackView的约束。
然后,这里和distribution有关的,只有[self.distributionStrategy alignView:view afterView:previousView]
这一个方法了,进入到这个方法里:
在OAStackViewDistributionStrategy.m中:
- (void)alignView:(UIView*)view afterView:(UIView*)previousView {
if (!previousView && !view) { return; }
if (!previousView) {
return [self alignFirstView:view];
}
if(!view) {
return [self alignLastView:previousView];
}
if (previousView && view) {
[self alignMiddleView:view afterView:previousView];
}
}
胜利就在眼前,这里添加相等约束的方法是[self alignMiddleView:view afterView:previousView]
的操作(怎么知道的,断点一步一步走到这里的)command+左键点击该方法,弹出框,选择alignMiddleView:(UIView*)view afterView:(UIView*)previousView
方法,因为之前已经设定了distribution属性值为OAStackViewDistributionFillEqually。
我们发现这里刚刚好是实现两个视图相等约束的方法。
如何验证呢?回到下断点后第一次跳转到得函数里,在[self.alignmentStrategy alignView:view withPreviousView:previousView];
下一个新断点,然后点击运行
两次(因为有两个Label,要循环运行两次,才会运行到这),然后在控制台输出stackView的约束。
相等的约束确实已经添加上来了,这样视图就可以等分了。那么,间隔是如何实现的呢?其实是设定space属性,改变视图见的约束值来实现的。默认为0,如果有新值传进来,则改成新值。其实这些都可以在storyboard中拖拽视图,添加约束来验证。
最后,说下OAStackView初始化后,是如何给子视图添加约束的。其实,主要就是下面这个回调,轮询给子视图添加约束。
在OAStackView+Traversal.h中声明
- (void)iterateVisibleViews:(void (^) (UIView *view, UIView *previousView))block;
在OAStackView+Traversal.m中定义
- (void)iterateVisibleViews:(void (^) (UIView *view, UIView *previousView))block {
id previousView;
for (UIView *view in self.subviews) {
if (view.isHidden) { continue; }
block(view, previousView);
previousView = view;
}
}
在在OAStackView.m中实施
- (void)layoutArrangedViews {
[self removeDecendentConstraints];
[self iterateVisibleViews:^(UIView *view, UIView *previousView) {
[self.distributionStrategy alignView:view afterView:previousView];
[self.alignmentStrategy addConstraintsOnOtherAxis:view];
}];
[self.distributionStrategy alignView:nil afterView:[self lastVisibleItem]];
}