附注:标题中“优雅地”三个字体现了作者对逼格的不懈追求。呵呵,装个逼,下面开始正题。
自动布局简介
一开始iOS写界面布局是通过frame固定位置及尺寸的。因为当时iPhone手机的屏幕尺寸是统一的,但后来随着iPhone手机尺寸变多,苹果新增了autoresizingMask
这个东西,它其实就是UIView的一个属性,意为“自动伸缩”。通过它能进行简单的自动布局,主要是完成父子视图之间的自动布局,但对于非父子视图等比较复杂的视图之间的自动布局,就显得力不从心了。随后,苹果便推出了功能更加强大的AutoLayout
,它几乎能完成任何复杂关系的布局。这便是自动布局的发展简介。
UIView的autoresizingMask
属性的定义:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
可以看到它是支持位运算符的枚举,也即可以像下面这样使用。它代表该视图greenView相对于父视图,它的宽和高是可以伸缩的,并非固定的。
greenView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
AutoLayout的约束——NSLayoutConstraint
一句话解释AutoLayout是通过给某视图添加“约束”来布局的,这些“约束”确定了该视图与其他视图的位置关系及自身尺寸等问题。
好了,现在假如我们想把一个UIView以这样的形式布局:该UIView的顶部距离父视图顶部100px,左边距离父视图的左边20px,右边距离父视图也20px,而其高度为300px。
AutoLayout的代码要这样写:
UIView *greenView = [[UIView alloc] init];
greenView.backgroundColor = [UIColor greenColor];
[self.view addSubview:greenView];
// greenView上边距view上边80
NSLayoutConstraint *constraint_Top = [NSLayoutConstraint constraintWithItem:greenView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.f
constant:80.f];
// greenView左边距view左边20
NSLayoutConstraint *constraint_Left = [NSLayoutConstraint constraintWithItem:greenView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeft
multiplier:1.f
constant:20.f];
// greenView右边距view右边20
NSLayoutConstraint *constraint_Right = [NSLayoutConstraint constraintWithItem:greenView
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeRight
multiplier:1.f
constant:-20.f];
// greenView的高度为300
NSLayoutConstraint *constraint_Height = [NSLayoutConstraint constraintWithItem:greenView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:0
multiplier:1.f
constant:400.f];
greenView.translatesAutoresizingMaskIntoConstraints = NO;
[greenView addConstraint:constraint_Height];
[self.view addConstraints:@[constraint_Top, constraint_Left, constraint_Right]];
看完上面的代码你也许该说该“What The Fuck?!”了,心想,写个这么简单的布局竟然要写这么一大段代码吗?
确实,如果在项目实践中,直接使用苹果提供的这一套API进行开发,简直是不可想象的,估计没人会这么做吧。但我们可以基于此进行封装,使其提供的API对于程序员来说语义明了,而且最好短小精悍。
不过在进行封装前,我们还得先把一些东西搞清楚。请继续往下看。
约束应该添加给哪个UIView?
可以注意到上面的代码里,与父视图有相对关系的“约束”是添加给父视图self.view
的。而表示greenView
高度的约束却是添加给greenView
的。这又是为什么?这里面有什么道理呢?
其中的道理是:如果是设置宽、高的固定长度,而和别的视图没有相对关系,就把该约束添加给自己;若该约束是和别的视图的一种相对关系,则添加给两个视图的最小公共父视图。
上面的例子里,greenView
的高度只是设置自己的高度,和其他视图并无关系,所以把约束添加给greenView
自己,而其他三个约束分别代表和父视图self.view
top/left/right的三个方向边距,是和别的视图有相对关系的,所以要添加给两者中的公共父视图,即self.view
。
拷贝几张图来体会:
开始封装AutoLayout
我们进行布局时,常常描述为:bView的top距离aView的bottom60px。或者bView的left距离aView的right20px。
总之它的思维方式是bView的top/bottom/left/right和aView的top/bottom/left/right的一种距离关系。
我们新建一个AutoLayoutObject
类来表示“约束”。
** AutoLayoutObject.h **
#import
#import
#define LAYEqual(a1, a2, m, c) [a1 addEqualConstraint:a2 multiplier:m constant:c]
#define LAYLessThan(a1, a2, m, c) [a1 addLessThanConstraint:a2 multiplier:m constant:c]
#define LAYMoreThan(a1, a2, m, c) [a1 addMoreThanConstraint:a2 multiplier:m constant:c]
#define LAYEqualC(a, c) [a addEqualConstant:c]
#define LAYLessThanC(a, c) [a addLessThanConstant:c]
#define LAYMoreThanC(a, c) [a addMoreThanConstant:c]
#define LAYM(a, m) [a addEqualMultiplier:m]
@interface AutoLayoutObject : NSObject
@property (nonatomic, strong)id item; // 某视图
@property (nonatomic, assign)NSLayoutAttribute attribute; // 属性,top/bottom/left/right/centerX/centerY等
@property (nonatomic, assign)NSLayoutRelation relation; // 等于 or 小于等于 or 大于等于
@property (nonatomic, assign)CGFloat multiplier; // 伸缩形变量
@property (nonatomic, assign)CGFloat constant; // 常量,代表长度或距离
// 添加等于某固定长度的约束 (某UIView的高为200)
- (void)addEqualConstant:(CGFloat)constant;
// 添加小于等于某固定长度的约束 (某UIView的高最大为200)
- (void)addLessThanConstant:(CGFloat)constant;
// 添加大于等于某固定长度的约束 (某UIView的高最小为200)
- (void)addMoreThanConstant:(CGFloat)constant;
// 添加两者等于的约束 (bView的top和aView的bottom的间距等于20)
- (void)addEqualConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;
// 添加A小于等于B的约束 (bView的top和aView的bottom的间距最大为20)
- (void)addLessThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;
// 添加A大于等于B的约束 (bView的top和aView的bottom的间距最小为20)
- (void)addMoreThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;
@end
** AutoLayoutObject.m **
#import "AutoLayoutObject.h"
#import "UIView+AutoLayout.h"
@implementation AutoLayoutObject
- (id)init
{
self = [super init];
if(self)
{
// 赋初值(默认值)
_item = nil;
_attribute = NSLayoutAttributeNotAnAttribute;
_relation = NSLayoutRelationEqual; // 默认是相等
_multiplier = 1.f;
_constant = 0.f;
}
return self;
}
// 添加等于某固定长度的约束 (某UIView的高为200)
- (void)addEqualConstant:(CGFloat)constant
{
UIView *superView = (UIView *)[self.item superview];
switch (self.attribute)
{
// left/right/top/bottom/centerX/centerY这些在不指定secondItem情况下,是firstItem以同样的状态把父类作为secondItem的。
case NSLayoutAttributeLeft:{
[self addEqualConstraint:superView.left multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeRight:{
[self addEqualConstraint:superView.right multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeTop:{
[self addEqualConstraint:superView.top multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeBottom:{
[self addEqualConstraint:superView.bottom multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeCenterX:{
[self addEqualConstraint:superView.centerX multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeCenterY:{
[self addEqualConstraint:superView.centerY multiplier:1.f constant:constant];
break;
}
// 设width 和 heigth为常量值
case NSLayoutAttributeWidth:
case NSLayoutAttributeHeight:{
[self addEqualConstraint:nil multiplier:1.f constant:constant];
break;
}
default:
break;
}
}
// 添加小于等于某固定长度的约束 (某UIView的高最大为200)
- (void)addLessThanConstant:(CGFloat)constant
{
UIView *superView = (UIView *)[self.item superview];
switch (self.attribute)
{
// left/right/top/bottom/centerX/centerY这些在不指定secondItem情况下,是firstItem以同样的状态把父类作为secondItem的。
case NSLayoutAttributeLeft:{
[self addLessThanConstraint:superView.left multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeRight:{
[self addLessThanConstraint:superView.right multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeTop:{
[self addLessThanConstraint:superView.top multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeBottom:{
[self addLessThanConstraint:superView.bottom multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeCenterX:{
[self addLessThanConstraint:superView.centerX multiplier:1.f constant:constant];
break;
}
case NSLayoutAttributeCenterY:{
[self addLessThanConstraint:superView.centerY multiplier:1.f constant:constant];
break;
}
// 设width 和 heigth为常量值
case NSLayoutAttributeWidth:
case NSLayoutAttributeHeight:{
[self addLessThanConstraint:nil multiplier:1.f constant:constant];
break;
}
default:
break;
}
}
// 添加大于等于某固定长度的约束 (某UIView的高最小为200)
- (void)addMoreThanConstant:(CGFloat)constant
{
// 省略...
}
// 添加两者等于的约束 (bView的top和aView的bottom的间距等于20)
- (void)addEqualConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
[self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationEqual];
}
// 添加A小于等于B的约束 (bView的top和aView的bottom的间距最大为20)
- (void)addLessThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
[self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationLessThanOrEqual];
}
// 添加A大于等于B的约束 (bView的top和aView的bottom的间距最小为20)
- (void)addMoreThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
[self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationGreaterThanOrEqual];
}
// 全能方法
- (void)addGenericConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant relation:(NSLayoutRelation)relation
{
if(!self.item){
return;
}
// 确定出视图aView和bView的最小公共父类ancestorView,最终要给该视图添加约束
UIView *aView = self.item;
UIView *bView = secondLayoutObj.item;
UIView *ancestorView;
if([aView isDescendantOfView:bView]){
ancestorView = bView;
}else if([bView isDescendantOfView:aView]){
ancestorView = aView;
}else if([aView isDescendantOfView:bView.superview]){
ancestorView = bView.superview;
}else if([bView isDescendantOfView:aView.superview]){
ancestorView = aView.superview;
}else{
ancestorView = aView.superview;
}
NSLayoutConstraint *layoutConstraint = [NSLayoutConstraint constraintWithItem:self.item
attribute:self.attribute
relatedBy:relation
toItem:secondLayoutObj.item
attribute:secondLayoutObj.attribute
multiplier:multiplier
constant:constant];
[ancestorView addConstraint:layoutConstraint];
}
@end
但是为了得到某UIView的这个自定义的“约束”对象,我们得提前新建一个UIView的分类:
#import
#import "AutoLayoutObject.h"
@interface UIView (AutoLayout)
+ (id)create;
- (AutoLayoutObject *)top;
- (AutoLayoutObject *)bottom;
- (AutoLayoutObject *)left;
- (AutoLayoutObject *)right;
- (AutoLayoutObject *)leading;
- (AutoLayoutObject *)trailing;
- (AutoLayoutObject *)width;
- (AutoLayoutObject *)height;
- (AutoLayoutObject *)centerX;
- (AutoLayoutObject *)centerY;
- (AutoLayoutObject *)baseline;
- (AutoLayoutObject *)notAnAttribute;
- (void)removeAllConstraint;
- (void)removeAutoLayoutObject:(AutoLayoutObject *)constraint;
@end
在UIView+AutoLayout
中我们不仅写了返回view各种“约束”对象的接口方法,而且也提供了初始化UIView的类方法,这不但使初始化一个UIView更简洁,而且主要的目的是为了关闭view的autoresizingMask
属性。
#import "UIView+AutoLayout.h"
@implementation UIView (AutoLayout)
+ (id)create
{
UIView *aView = [[self alloc] init];
if(aView){
aView.translatesAutoresizingMaskIntoConstraints = NO; // 默认是支持autoresizingMask的,但它可能会和autoLayout冲突,所以我们关闭autoresizingMask属性。
}
return aView;
}
- (AutoLayoutObject *)top
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeTop];
}
- (AutoLayoutObject *)bottom
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeBottom];
}
- (AutoLayoutObject *)left
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeLeft];
}
- (AutoLayoutObject *)right
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeRight];
}
- (AutoLayoutObject *)leading
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeLeading];
}
- (AutoLayoutObject *)trailing
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeTrailing];
}
- (AutoLayoutObject *)width
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeWidth];
}
- (AutoLayoutObject *)height
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeHeight];
}
- (AutoLayoutObject *)centerX
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeCenterX];
}
- (AutoLayoutObject *)centerY
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeCenterY];
}
- (AutoLayoutObject *)baseline
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeBaseline];
}
- (AutoLayoutObject *)notAnAttribute
{
return [self autoLayoutObjWithAttribute:NSLayoutAttributeNotAnAttribute];
}
- (void)removeAllConstraint
{
[self removeAllConstraint];
}
- (void)removeAutoLayoutObject:(AutoLayoutObject *)autoLayoutObj
{
for(NSLayoutConstraint *constranit in self.constraints)
{
if(autoLayoutObj.attribute == constranit.firstAttribute){
[self removeConstraint:constranit];
}
}
}
// 生成当前UIView的autoLayoutObj对象
- (AutoLayoutObject *)autoLayoutObjWithAttribute:(NSLayoutAttribute)attribute
{
AutoLayoutObject *autoLayoutObj = [[AutoLayoutObject alloc] init];
autoLayoutObj.item = self;
autoLayoutObj.attribute = attribute;
return autoLayoutObj;
}
@end
以上AutoLayoutObject
类和UIView分类UIView+AutoLayout
是便是完成自动布局的全部东西。
好了,我们通过这个封装后的自动布局来完成本文开头的那个布局。是不是精炼了很多。(我们把已然封装后提供的接口方法再次进行了宏定义,最终我们使用与布局方法相对应的宏来进行布局,这样更简短,更明了。)
UIView *greenViewiew = [UIView create];
greenViewiew.backgroundColor = [UIColor greenColor];
[self.view addSubview:greenViewiew];
LAYEqual(greenViewiew.top, self.view.top, 1, 80.f);
LAYEqual(greenViewiew.left, self.view.left, 1, 20.f);
LAYEqual(greenViewiew.right, self.view.right, 1, -20.f);
LAYEqualC(greenViewiew.height, 300.f);
约束为一个范围的情况
我们在实际开发中经常会碰到,某视图布局的需求是最大不得超过多少,且最小不得小于多少。是在一个范围内的。
比如一个UILabel
,它里面的文字很多时,正常情况下系统会自动根据文本的多少来更新高度,而不需要你自己添加约束。
UILabel *lab = [UILabel create];
lab.numberOfLines = 0;
lab.text = @"经济学的理论建立在这样一种假设上:人们在经济交往中企图以最小的代价获取最大的利润。但市场中的交换其实不过是社会交换中的一种典型。广泛的社会交换同样建立在这样假设上:人们在社会生活总也是企图以最小代价获取最大利益的。所不同的是经济生活中的代价和报偿是有形的,表现为金钱和物质,而社会交往中的代价和报偿在很多时间和场合中是无形的,如友情,义务,声望,权威。";
lab.backgroundColor = [UIColor greenColor];
[self.view addSubview:lab];
LAYEqual(lab.left, self.view.left, 1, 30.f);
LAYEqual(lab.right, self.view.right, 1, -30.f);
LAYEqual(lab.top, self.view.top, 1, 100.f);
此时不需要你约束该lab
的高度,它会根据文本多少来自动更新高度。
但是往往需求中,产品经理要求这里文字很多时,该视图也不能无限制变高,得有个限度;当文字很少时,高度也不能太小,也要有个限度。这个在我们上面的封装的自动布局中是支持的。
UILabel *lab = [UILabel create];
lab.numberOfLines = 0;
lab.text = @"经济学的理论建立在这样一种假设上:人们在经济交往中企图以最小的代价获取最大的利润。但市场中的交换其实不过是社会交换中的一种典型。广泛的社会交换同样建立在这样假设上:人们在社会生活总也是企图以最小代价获取最大利益的。所不同的是经济生活中的代价和报偿是有形的,表现为金钱和物质,而社会交往中的代价和报偿在很多时间和场合中是无形的,如友情,义务,声望,权威。";
lab.backgroundColor = [UIColor greenColor];
[self.view addSubview:lab];
LAYEqual(lab.left, self.view.left, 1, 30.f);
LAYEqual(lab.right, self.view.right, 1, -30.f);
LAYEqual(lab.top, self.view.top, 1, 100.f);
LAYMostC(lab.height, 100.f);
即便文字很多,高度也为100.f这个上限:
关于自动布局的代码我也上传至GitHub
上了,求Star
支持。
YWAutoLayout
GitHub地址