iOS开发屏幕适配篇——masonry简介
一、为什么要学masonry(why)?
目前iOS开发中大多数页面都已经开始使用Interface Builder的方式进行UI开发了,但是在一些变化比较复杂的页面,还是需要通过代码来进行UI开发的。 而且有很多比较老的项目,本身就还在采用纯代码的方式进行开发。 而现在iPhone和iPad屏幕尺寸越来越多,虽然开发者只需要根据屏幕点进行开发,而不需要基于像素点进行UI开发。但如果在项目中根据不同屏幕尺寸进行各种判断, 写死坐标的话,这样开发起来是很吃力的。 所以一般用纯代码开发UI的话,一般都是配合一些自动化布局的框架进行屏幕适配。苹果为我们提供的适配框架有:VFL、UIViewAutoresizing、Auto Layout、 Size Classes等。 其中Auto Layout是使用频率最高的布局框架,但是其也有弊端。就是在使用UILayoutConstraint的时候,会发现代码量很多,而且大多都是重复性的代码, 以至于好多人都不想用这个框架。 后来Github上的出现了基于UILayoutConstraint封装的第三方布局框架Masonry,Masonry使用起来非常方便,本篇文章就详细讲一下Masonry的使用。
二、masonry是什么(what)?
Masonry是一个对系统NSLayoutConstraint进行封装的第三方自动布局框架,采用链式编程的方式提供给开发者API。系统AutoLayout支持的操作,Masonry都支持, 相比系统API功能来说,Masonry是有过之而无不及。 Masonry采取了链式编程的方式,代码理解起来非常清晰易懂,而且写完之后代码量看起来非常少。之前用NSLayoutConstraint写很多代码才能实现的布局, 用Masonry最少一行代码就可以搞定。下面看到Masonry的代码就会发现,太简单易懂了。 Masonry是同时支持Mac和iOS两个平台的,在这两个平台上都可以使用Masonry进行自动布局。我们可以从MASUtilities.h文件中,看到下面的定义, 这就是Masonry通过宏定义的方式,区分两个平台独有的一些关键字。 支持的属性: @property (nonatomic, strong, readonly) MASConstraint *left; @property (nonatomic, strong, readonly) MASConstraint *top; @property (nonatomic, strong, readonly) MASConstraint *right; @property (nonatomic, strong, readonly) MASConstraint *bottom; @property (nonatomic, strong, readonly) MASConstraint *leading; @property (nonatomic, strong, readonly) MASConstraint *trailing; @property (nonatomic, strong, readonly) MASConstraint *width; @property (nonatomic, strong, readonly) MASConstraint *height; @property (nonatomic, strong, readonly) MASConstraint *centerX; @property (nonatomic, strong, readonly) MASConstraint *centerY; @property (nonatomic, strong, readonly) MASConstraint *baseline;
三、怎样学习masonry(how)?
1.masonry遇到的坑
在使用Masonry进行约束时,不用设置weak控制器self,因为masonry的内容使用的不是block,不存在循环引用。 在使用Masonry进行约束时,有一些是需要注意的。 在使用Masonry添加约束之前,需要在addSubview之后才能使用,否则会导致崩溃。 在添加约束时初学者经常会出现一些错误,约束出现问题的原因一般就是两种:约束冲突和缺少约束。对于这两种问题,可以通过调试和log排查。 之前使用Interface Builder添加约束,如果约束有错误直接就可以看出来,并且会以红色或者黄色警告体现出来。而Masonry则不会直观的体现出来,而是以运行过程中崩溃或者打印异常log体现,所以这也是手写代码进行AutoLayout的一个缺点。 这个问题只能通过多敲代码,积攒纯代码进行AutoLayout的经验,慢慢就用起来越来越得心应手了。
Masonry 设置UILabel时多行一定要设置宽度。
2.基础API
mas_makeConstraints() 添加约束 mas_remakeConstraints() 移除之前的约束,重新添加新的约束 mas_updateConstraints() 更新约束 equalTo() 参数是对象类型,一般是视图对象或者mas_width这样的坐标系对象 mas_equalTo() 和上面功能相同,参数可以传递基础数据类型对象,可以理解为比上面的API更强大 width() 用来表示宽度,例如代表view的宽度 mas_width() 用来获取宽度的值。和上面的区别在于,一个代表某个坐标系对象,一个用来获取坐标系对象的值
3.修饰语句
Auto Boxing 上面例如equalTo或者width这样的,有时候需要涉及到使用mas_前缀,这在开发中需要注意作区分。 如果在当前类引入#import "Masonry.h"之前,用下面两种宏定义声明一下,就不需要区分mas_前缀。 // 定义这个常量,就可以不用在开发过程中使用"mas_"前缀。 #define MAS_SHORTHAND // 定义这个常量,就可以让Masonry帮我们自动把基础数据类型的数据,自动装箱为对象类型。 #define MAS_SHORTHAND_GLOBALS 修饰语句 Masonry为了让代码使用和阅读更容易理解,所以直接通过点语法就可以调用,还添加了and和with两个方法。这两个方法内部实际上什么都没干,
只是在内部将self直接返回,功能就是为了更加方便阅读,对代码执行没有实际作用。 例如下面的例子: make.top.and.bottom.equalTo(self.containerView).with.offset(padding);
3.常用的方法
设置内边距 /** 设置yellow视图和self.view等大,并且有10的内边距。 注意根据UIView的坐标系,下面right和bottom进行了取反。所以不能写成下面这样,否则right、bottom这两个方向会出现问题。 make.edges.equalTo(self.view).with.offset(10); 除了下面例子中的offset()方法,还有针对不同坐标系的centerOffset()、sizeOffset()、valueOffset()之类的方法。 */ [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).with.offset(10); make.top.equalTo(self.view).with.offset(10); make.right.equalTo(self.view).with.offset(-10); make.bottom.equalTo(self.view).with.offset(-10); }]; 通过insets简化设置内边距的方式 // 下面的方法和上面例子等价,区别在于使用insets()方法。 [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { // 下、右不需要写负号,insets方法中已经为我们做了取反的操作了。 make.edges.equalTo(self.view).with.insets(UIEdgeInsetsMake(10, 10, 10, 10)); }];
4.常用例子
4.1 上下排列
[self.view addSubview:self.blueView]; [self.view addSubview:self.yellowView]; [self.view addSubview:self.redView]; //三个等高视图占一个屏幕,关键点inses内边距和高度可以同时等同于多个视图make.height.equalTo(@[self.redView,self.blueView]); //上下排列核心思想:起点redView视图固定上左右底,中间blueView固定左右底,底部yellowView固定 左右底,高度等高 1.基本用法 CGFloat padding = 10; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.top.equalTo(self.view).insets(UIEdgeInsetsMake(padding, padding, 0, padding)); make.bottom.equalTo(self.blueView.mas_top).offset(-10); }]; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, 0, padding)); make.bottom.equalTo(self.yellowView.mas_top).offset(-10); }]; [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, padding, padding)); make.height.equalTo(@[self.redView,self.blueView]); }]; #pragma mark - 🔒private - (NSString *)ranomText{ CGFloat length = arc4random()%50 +5; NSMutableString *str = [[NSMutableString alloc] init]; for (NSInteger i = 0; i <length; i++) { [str appendString:@"测试一下啦!"]; } return str; } - (UIView *)blueView{ if (!_blueView) { _blueView = [[UIView alloc] init]; [_blueView setBackgroundColor:[UIColor blueColor]]; } return _blueView; } - (UIView *)yellowView{ if (!_yellowView) { _yellowView = [[UIView alloc] init]; [_yellowView setBackgroundColor:[UIColor yellowColor]]; } return _yellowView; } - (UIView *)redView{ if (!_redView) { _redView = [[UIView alloc] init]; [_redView setBackgroundColor:[UIColor redColor]]; } return _redView; }
4.2 左右排列
//左左排列核心思想:起点blueView固定中心、左右,高度及等宽,中间redView固定高度,中心及右边,最右边视图等高,右,中心位置 CGFloat padding = 10.0f; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view.mas_left).offset(padding); make.centerY.equalTo(self.view.mas_centerY); make.height.equalTo(@50); make.width.equalTo(@[self.redView,self.yellowView]); make.right.equalTo(self.redView.mas_left).with.offset(-padding); }]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@50); make.right.equalTo(self.yellowView.mas_left).offset(-padding); make.centerY.equalTo(self.view.mas_centerY); }]; [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(@[self.blueView,self.redView]); make.height.equalTo(@[self.blueView,self.redView]); make.right.equalTo(self.view.mas_right).offset(-10); }];
4.3 上下复合排列
//左右下核心思想,首先总体结构是上下所以要设置等高,然后再设置上下左右的间隙,左1设置上左下右及高度,右1设置右下上就可以因为左边已经有约束,下blueView只需要设置左右下及高度 CGFloat padding = 10.0f; [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.top.equalTo(self.view).insets(UIEdgeInsetsMake(padding, padding, 0, 0)); make.right.equalTo(self.redView.mas_left).offset(-padding); make.bottom.equalTo(self.blueView.mas_top).offset(-padding); make.width.equalTo(self.redView.mas_width); }]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.right.equalTo(self.view).insets(UIEdgeInsetsMake(padding, 0,0,padding)); make.bottom.equalTo(self.blueView.mas_top).offset(-padding); }]; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, 0, padding)); make.bottom.equalTo(self.view.mas_bottom); make.height.equalTo(self.yellowView.mas_height); }];
4.4 约束比例, multipliedBy 只能设置同一控制的,如自身高度等于宽度的3倍,宽度*这个比例
UIView *topView = [[UIView alloc] init]; topView.backgroundColor = [UIColor redColor]; [self.view addSubview:topView]; UIView *topInnerView = [[UIView alloc] init]; topInnerView.backgroundColor = [UIColor greenColor]; [topView addSubview:topInnerView]; UIView *bottomView = [[UIView alloc] init]; bottomView.backgroundColor = [UIColor blackColor]; [self.view addSubview:bottomView]; UIView *bottomInnerView = [[UIView alloc] init]; bottomInnerView.backgroundColor = [UIColor blueColor]; [bottomView addSubview:bottomInnerView]; [topView mas_makeConstraints:^(MASConstraintMaker *make) {~~ make.left.right.top.equalTo(self.view).width.offset(0); make.height.mas_equalTo(bottomView); }]; [topInnerView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(topView); make.width.equalTo(topInnerView.mas_height).multipliedBy(3); make.center.equalTo(topView); //设置优先级,设置优先级时一定要加(),优先级 Masonry会优先实现优先级高的设定,发生冲突时,放弃优先级低的设定. make.width.height.mas_equalTo(topView).priorityLow(); make.width.height.lessThanOrEqualTo(topView); }]; [bottomView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.bottom.equalTo(self.view); make.top.equalTo(topView.mas_bottom); }]; [bottomInnerView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.mas_equalTo(bottomView); make.center.equalTo(bottomView); make.height.equalTo(bottomInnerView.mas_width).multipliedBy(3); //设置优先级,lessThanOrEqualTo小于等于,优先级 Masonry会优先实现优先级高的设定,发生冲突时,放弃优先级低的设定. make.height.width.equalTo(bottomView).priorityLow(); make.height.width.lessThanOrEqualTo(bottomView); }];
4.5 点击放大,更新约束的使用
/**3.点击放大,知识点,更新约束,优先级,小于等于等 核心1://初始化宽度,高为100,优先级最低 make.width.height.mas_equalTo(100*self.scale).priorityLow(); //最大放大到整个view,小于等于view的宽度和高度 make.width.height.lessThanOrEqualTo(self.view); 核心2:更新约束 ,告诉需要更新,检查是否需要更新,立即更新*/ @property (nonatomic, strong) UIButton *bigButton; @property (nonatomic, assign) CGFloat scale;//比例 self.scale = 1.0; self.bigButton = [UIButton buttonWithType:UIButtonTypeCustom]; [self.bigButton setTitle:@"点我放大" forState:UIControlStateNormal]; self.bigButton.layer.borderColor = UIColor.greenColor.CGColor; self.bigButton.layer.borderWidth = 3; [self.bigButton addTarget:self action:@selector(onGrowButtonTaped:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.bigButton]; [self.bigButton mas_makeConstraints:^(MASConstraintMaker *make) { make.center.mas_equalTo(self.view); //初始化宽度,高为100,优先级最低 make.width.height.mas_equalTo(100*self.scale).priorityLow(); //最大放大到整个view make.width.height.lessThanOrEqualTo(self.view); }]; #pragma mark - 🎬event response - (void)onGrowButtonTaped:(UIButton *)sender{ self.scale += 0.2; //告诉self.view约束需要更新 [self.view setNeedsUpdateConstraints]; // 调用此方法告诉self.view检测是否需要更新约束,若需要则更新,下面添加动画效果才起作用 [self.view updateConstraintsIfNeeded]; [UIView animateWithDuration:0.3 animations:^{ [self.view layoutIfNeeded]; //立即更新 }]; } #pragma mark - updateViewConstraints - (void)updateViewConstraints{ [self.bigButton mas_updateConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); //设置优先级 make.width.height.mas_equalTo(100*self.scale).priorityLow(); make.width.height.lessThanOrEqualTo(self.view); }]; //调用父类更新约束 [super updateViewConstraints]; }
4.6 scrollView的简单适配
/**4. scrollView,核心思想就是先固定四周,然后再设置contentSize,在这里是底部 核心1:preferredMaxLayoutWidth 最大的适配宽度 核心2:scrollView的适配,首先先固定,然后设置底部的值 核心3:配置label顶部间距 */ self.scrollView = [[UIScrollView alloc] init]; self.scrollView.pagingEnabled = NO; [self.view addSubview:self.scrollView]; [self.scrollView setBackgroundColor:[UIColor lightGrayColor]]; CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; UILabel *lastLabel = nil; for (NSInteger i = 0; i < 20; i++) { UILabel *label = [[UILabel alloc] init]; label.numberOfLines = 0; label.layer.borderColor = [UIColor greenColor].CGColor; label.layer.borderWidth = 2.0; label.text = [self ranomText]; label.preferredMaxLayoutWidth = screenWidth - 30; label.textAlignment = NSTextAlignmentLeft; [self.scrollView addSubview:label]; [label mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view.mas_left).offset(15); make.right.equalTo(self.view.mas_right).offset(-15); if (lastLabel) {//下一行是第一行最后一个label加上一定的间距 make.top.mas_equalTo(lastLabel.mas_bottom).offset(20); }else{ make.top.mas_equalTo(self.scrollView.mas_top).offset(20); } }]; lastLabel = label; } [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); make.bottom.equalTo(lastLabel.mas_bottom).offset(20); }]; #pragma mark - 🔒private - (NSString *)ranomText{ CGFloat length = arc4random()%50 +5; NSMutableString *str = [[NSMutableString alloc] init]; for (NSInteger i = 0; i <length; i++) { [str appendString:@"测试一下啦!"]; } return str; }
将来的自己,会感谢现在不放弃的自己!