1、实现约束规则之间的对比
所有的约束规则都遵循同一套固定的结构,而且都有相关的优先级:
view1.attribute(relation)view2.attribute*multiplier + constant
上述等式的每个部分都与NSLayoutConstraint对象的属性相对应,他们分别是priority、firstItem。firstAttribute、relation、secondItem、secondAttribute、multiplier与constant。通过这些属性,我们很容易比对两条约束规则。
UIView会把约束规则当成NSLayoutConstraint对象来保存或者移除。即便两条规则所描述的含义相同,只要其内存位置不同,他们就是不相等的。如果实现了规则之间的对比功能,那么就必须将规则专门存放的变量里面了,而是可以通过程序吗随时添加并移除它们。
下面写了三个方法。constraint:matches:方法会比较两条约束规则的相关属性,以此判断两者是否相同。请注意,在
比较的时候,我们只考虑等式本身,而不考虑优先级,因为两条约束规则只要描述的是同一套约束方法,那么无论开发者给其指定何种优先级,他们实际上都是等效的。
还有两个方法分别叫作constraintMatchingConstraint:及removeMatchingConstraint:,前者在视图里查找与开发者所给规则相匹配的首条约束规则,而后者则把此约束规则从视图中移除。
下面会把某条约束规则从视图里删除,并换上一条新约束规则,这样的话,此视图就会从上级视图的顶部跑到上级视图的底部了。对于这个简单的情况来说,我们完全可以把约束规则保存到实例变量中,并在稍后将其从视图里移除。不过,在需要管理多条约束规则或动态移除某约束规则的时候,这种相似约束规则的查询与移除功能还是非常有用的。
- (BOOL)constraint:(NSLayoutConstraint *)constraint1 matches:(NSLayoutConstraint *)constraint2
{
if (constraint1.firstItem != constraint2.firstItem) {
return NO;
}
if (constraint1.secondItem != constraint2.secondItem) {
return NO;
}
if (constraint1.firstAttribute != constraint2.firstAttribute) {
return NO;
}
if (constraint1.secondAttribute != constraint2.secondAttribute) {
return NO;
}
if (constraint1.relation != constraint2.relation) {
return NO;
}
if (constraint1.multiplier != constraint2.multiplier) {
return NO;
}
if (constraint1.constant != constraint2.constant) {
return NO;
}
return YES;
}
- (NSLayoutConstraint *)constrainMatchingConstraint:(NSLayoutConstraint *)aConstraint
{
for (NSLayoutConstraint *constraint in self.constraints) {
if ([self constraint:constraint matches:aConstraint]) {
return constraint;
}
}
for (NSLayoutConstraint *constraint in self.superview.constraints) {
if ([self constraint:constraint matches:aConstraint]) {
return constraint;
}
}
return nil;
}
- (void)removeMatchingConstraint:(NSLayoutConstraint *)aConstraint
{
NSLayoutConstraint *match = [self constrainMatchingConstraint:aConstraint];
if (match) {
[self removeConstraint:match];
[self.superview removeConstraint:match];
}
}
2、创建尺寸固定且受规则约束的视图
1、禁用translatesAutoresizingMaskIntoConstraints
autoresizing可以指程序代码里用到的一些相关标志,例如UIViewAutoresizingFlexibleWidth等。如果要通过这些手段来指定视图的自动调整行为,那么在定义约束规则的时候,就不应该再引用该视图了。
使用约束规则之前,应该先禁用视图中的有关属性,该属性会把涉及自动调整功能的掩码自行转换为约束规则。如果启用该属性,那么视图会通过与自动调整功能有关的掩码来参与到Auto Layout系统里面,要是禁用它,开发者就需要自己添加约束规则。
这个与约束规则有关的属性叫作translatesAutoresizingMaskIntoConstraints。如果将其设为NO,我们就可以放心的向视图中添加自己的约束规则,而不必担心它会与系统自动生成的规则相冲突。这一点非常重要。若是尚未禁用该属性就直接开始添加约束,则可能会引发冲突。由自动调整功能所生成的那些规则无法与自己编写的这些规则和平共处。
2、令视图出现在上级视图范围内
下面第一个方法叫作constrainWithinSuperviewBounds,它会把某视图完全放在其上级视图的范围内。该方法创建了四条约束规则来保证这一点。其中一条规则要求视图的左边界不许与上级视图的左边界对齐,或位于其右侧,另一条规则要求本视图的顶端必须与上级视图的顶端对齐,或位于其下方,其余两条也与之相似。
之所以要创建这个方法,是因为在约束规则比较宽松的系统中,视图的原点完全有可能是负值,从而导致自己出现在屏幕之外而不为用户所见。该方法的基本意思是:在排布这个子视图的位置时,请考虑到(0,0)原点与上级视图的尺寸,而不要把它排布到上级视图外面去。
在日常开发中,我们不一定非要设置这套约束规则。但在刚开始接触约束并想通过代码来研究它的时候,这种约束规则就显得非常有用了。他能够确保本视图不会越出上级视图的范围,从而令我们可以测试其他各项约束规则,并观察这些规则之间的关系。
此外,在熟悉了约束系统之后,我们可能还想给程序添加一些调试用的反馈代码,以便在主视图加载完毕并运用了约束规则之后,能够知道我们自己的视图位于何处。比方说,可以把下面这个for循环添加到viewDidAppear:方法中:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
for (UIView *view in self.view.subviews) {
NSLog(@"View (%d) location:%@",[self.view.subviews indexOfObject:view],NSStringFromCGRect(view.frame));
}
}
3、限定视图的尺寸
下面第二个方法叫作constrainSize:,他会把视图的尺寸固定为开发者所指定的CGSize。在定义约束规则的时候,这是一种经常会用到的操作。我们不能再像原来那样设定视图的框架。另外也请记得:这些约束规则都只是一种请求,未必总是与最终的布局完全相符。假如没有合理的设计约束规则,那么宽度为100个点的文本框在最终的程序界面里其宽度可能会变成107个点,或更糟。
我们可以在约束规则中针对某视图来指定其宽度或高度,然而这两条规则所要用到的宽度值和高度值是不能预判的,因为constrainSize:方法要使用与各种视图才行。于是,我们把这两个值先放在metrics字典里,然后再传给constraintsWithVisualFormat。所谓指标,其实就是约束规则里所用到的一些数值变量。
这两条规则所用的两个指标分别叫作theHeight和theWidth,这些名称完全可以随机选取,开发者应该把字符串作为键,放在传给metrics:参数的字典里。在调用创建约束规则的那个方法时,把这个字典传进去。指标里的每个键必须出现在这份字典中,而且它所对应的值必须是个NSNumber。
方法里的两条约束规则设定了我们所期望的视图的宽度及视图的高度。两个格式字符串分别告诉约束系统当前这个视图在横轴和纵轴上的尺寸应该是多少。
第三个方法叫作constrainPosition:,他会构建两条约束规则,用于确定本视图的原点在上级视图里的位置。
4、把前面几点的内容拼装起来
我们要用约束规则把上述标签和其上级视图联系起来,然而在添加规则之前,必须先把标签控件作为子视图放到上级视图里。在其上添加约束规则的那个视图和规则中所提到的视图必须位于同一个视图体系中,甚至会令程序在运行的时候崩溃。所以,我们有时可能要稍稍调整一下代码顺序,比方说,上面这个方法就会在添加约束规则之前,先把标签放在上级视图中。
无论按何种顺序来调整视图的生成代码,我们都要遵循下列步骤:首先创建视图,然后将其添加到上级视图里,接下来禁用translatesAutoresizingMaskIntoConstraints,最后运用必要的约束规则。
- (void)constrainWithinSuperviewBounds
{
if (!self.superview) {
return;
}
[self.superview addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self.superview attribute:NSLayoutAttributeLeft multiplier:1 constant:0]];
[self.superview addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self.superview attribute:NSLayoutAttributeTop multiplier:1 constant:0]];
[self.superview addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationLessThanOrEqual toItem:self.superview attribute:NSLayoutAttributeRight multiplier:1 constant:0]];
[self.superview addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationLessThanOrEqual toItem:self.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0]];
}
- (void)constrainSize:(CGSize)aSize
{
NSMutableArray *array = [NSMutableArray array];
[array addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[self(theWith@750)]" options:0 metrics:@{@"theWidth":@(aSize.width)} views:NSDictionaryOfVariableBindings(self)]];
[array addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[self(theHeight@750)]" options:0 metrics:@{@"theWidth":@(aSize.height)} views:NSDictionaryOfVariableBindings(self)]];
[self addConstraints:array];
}
- (void)constrainPosition:(CGPoint)aPoint
{
if (!self.superview) {
return;
}
NSMutableArray *array = [NSMutableArray array];
[array addObject:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeLeft multiplier:1 constant:0]];
[array addObject:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeTop multiplier:1 constant:0]];
[self.superview addConstraints:array];
}
2、将两个视图居中对齐
如果想把两个视图居中对齐,可以将某视图的中心点属性同其容器的对应属性关联起来。有两个方法可以获得某视图的上级视图,并在这两个视图的中心点之间施加等同关系。
下面是两条简单的原则:
1、创建约束规则的时候,如果规则所涉及的某个视图是另一个视图的上级视图,那么就把规则添加到这个上级视图里。
2、如果用格式化字符串来创建约束规则,而字符串里面又包含表示上级视图的竖线,那么就把规则添加到对应的上级视图里。
- (void)centerHorizontallyInSuperview
{
if (!self.superview) {
return;
}
[self.superview addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
}
- (void)centerVerticallyInSuperview
{
if (!self.superview) {
return;
}
[self.superview addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
}
3、设定宽高比
在约束规则中使用放大倍数,也就是等式y=m*x+b中的m,即可设置视图的宽高比。
为了切换宽高比,这个方案会把设置好的约束规则保存到NSLayoutConstraint型的aspectConstraint变量里。用户每次把比例从16:9切换到4:3或是切换回去时,这个解决方案都会将前一条约束规则从视图里移除,然后创建新规则并将其保存起来。创建新规则的时候要设置适当的放大倍数,创建好之后,还要将其添加到视图里面。
我们既想令视图的宽度和高度可以灵活变化,又想使其保持一定的尺寸,所以,这条解决方案的createLabel方法需要做两件事。首先,要用谓词来限定宽度及高度,它请求系统吧宽度和高度都至少设为300个点。其次,要给这一请求指定优先级。我们把优先级设定的比较高,但是并没有设定成非满足不可,这样就给约束系统留下了继续调整布局的余地。做完这两件事之后,我们就实现了一套能够实时调整宽高比的程序界面,而且程序还可以在运行的时候动态改变其布局。
限定宽高比的约束规则也可以用来保持某张图像的横纵比例。视图的contentMode可能无法完全准确的设定图像的横纵比。我们可以通过UIImage的size属性得知图像尺寸,然后用其宽度除以高度,并据此创建出符合横纵比的约束规则来。
- (UILabel *)createLabelWithTitle:(NSString *)title onParent:(UIView *)parentView
{
UILabel *label = [[UILabel alloc] init];
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = [UIColor greenColor];
[parentView addSubview:label];
label.translatesAutoresizingMaskIntoConstraints = NO;
[label constrainWithinSuperviewBounds];
[label addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[label(>=theWidth@750)]" options:0 metrics:@{@"theWidth":@300} views:NSDictionaryOfVariableBindings(label)]];
label addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[label(>=theHeight@750)]" options:0 metrics:@{@"theHeight":@300} views:NSDictionaryOfVariableBindings(label)];
[label centerInSuperview];
return label;
}
- (void)toggleAspectRatio
{
if (aspectConstraint) {
[self.view removeConstraint:aspectConstraint];
}
if (useFourToThree) {
aspectConstraint = [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:view1 attribute:NSLayoutAttributeHeight multiplier:(4.0/3.0) constant:0];
}else{
aspectConstraint = [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:view1 attribute:NSLayoutAttributeHeight multiplier:(16.0/9.0) constant:0];
}
[self.view addConstraint:aspectConstraint];
useFourToThree = !useFourToThree;
}
4、响应屏幕方向的变更
设备屏幕的几何特征也会影响界面的布局方式。比方说,当设备处在横屏状态的时候,垂直方向上就没有太多的空间可以排布内容。
为了适应这两种布局,我们必须根据屏幕方向的变更情况来修改约束规则,并重新排版。updateViewControllerConstraints方法能够依照当前的屏幕方向来刷新约束规则,它会把已有的规则全部移除,并设置新的约束。我们应该在willAnimateRotationToInterfaceOrientation:duration:里面调用此方法。这样所产生的效果就可以和界面里的其他动画效果平滑的融合起来。另外要注意,系统是在把视图控制器的interfaceOrientation属性设置好之后,再去调用willAnimateRotationToInterfaceOrientation的,而不是像willRotateTointerfaceOrientation:duration:方法那样在该属性变更之前回调。这样的话,updateViewControllerConstraint方法所查找到的界面方向本身已经是正确的界面方向了,我们可以根据此生成适当的约束规则。
- (void)updateViewControllerConstraints
{
[self.view removeConstraints:self.view.constraints];
NSDictionary *bindings = NSDictionaryOfVariableBindings(imageView,titleLabel,artistLabel,button);
if (UIDeviceOrientationIsPortrait(self.interfaceOrientation) || (self.interfaceOrientation == UIDeviceOrientationUnknown)) {
for (UIView *view in @[imageView,titleLabel,artistLabel,button]) {
CENTER_VIEW_H(self.view,view);
}
CONSTRAIN_VIEWS(self.view,@"V:|-80-[imageView]-30-\[titleLabel(>=0)]-[artisLabel]-15-[button]-(>=0)-|",bindings);
}else{
CENTER_VIEW_V(self.view,imageView);
CONSTRAIN(self.view,imageView,@"H:|-[imageView]");
CONSTRAIN(self.view,titleLabel,@"H:[titleLabel]-15-|");
CONSTRAIN(self.view,artistLabel,@"H:[artistLabel]-15-|");
CONSTRAIN(self.view,button,@"H:[button]-15-|");
CONSTRAIN_VIEWS(self.view,@"V:|-(>=0)-[titleLabel(>=0)]\-[artistLabel]-15-[button]-|",bindings);
CONSTRAIN_VIEWS(self.view,@"H:[imageView]-(>=0)-[titleLabel]",bindings);
}
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self updateViewControllerConstraints];
}