本人有若干成套学习视频, 可试看! 可试看! 可试看, 重要的事情说三遍 包含Java
, 数据结构与算法
, iOS
, 安卓
, python
, flutter
等等, 如有需要, 联系微信tsaievan
.
说到Masonry的使用, 相信大家一定不会陌生, 但是我在使用Masonry的过程中还是有些懵逼的地方, 比如使用什么API, 在哪个地方添加约束等. 所以我就把Masonry的官方demo抄了一遍, 发现还是有很多值得注意的地方, 现在总结如下:
官方的Demo是严格将控制器和View分离了的, 你也可以理解为严格的MVC架构, 只是缺少了Model层.
对于一些简单的约束, 可以将约束直接写在init
方法里面, 或者initWithFrame:
指定构造方法里面
还需要注意的一点是:
注意: 添加子控件一定要放在设置约束之前, 不然约束就找不到约束对象
(一)最基本的使用:
先看如下代码:
UIView *superView = self;
int padding = 10;
[greenView mas_makeConstraints:^(MASConstraintMaker *make) {
// 大于等于
make.top.mas_greaterThanOrEqualTo(superView.mas_top).offset(padding);
make.left.mas_equalTo(superView.mas_left).offset(padding);
make.bottom.mas_equalTo(blueView.mas_top).offset(-padding);
make.right.mas_equalTo(redView.mas_left).offset(-padding);
make.width.mas_equalTo(redView.mas_width);
make.height.mas_equalTo(redView.mas_height);
make.height.mas_equalTo(blueView.mas_height);
}];
这也是我们最常用的几个API:
mas_makeConstraints
mas_remakeConstraints
mas_updateConstraints
稍后还会详细说这几个API
在我们设置约束的时候, 通常会用到
-
mas_greaterThanOrEqualTo
--------------≥ -
mas_lessThanOrEqualTo
-----------------≤ -
mas_equalTo
---------------------------=
这里面的参数可以传的类型很多, 你可以传一个UIView类型的指针, 也可以传MASViewAttribute
类型的属性, 可以传一个包含多个View的数组, 也可以传包含多个MASViewAttribute
类型的数组:
// 可以传一个包含views的数组
make.height.mas_equalTo(@[ greenView, blueView ]);
// 可以传一个包含属性的数组
make.height.mas_equalTo(@[ greenView.mas_height, redView.mas_height ]);
(二)常量
-
使用NSNumber类型以及CGFloat类型
[purpleView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(20);
make.left.mas_equalTo(20);
make.bottom.mas_equalTo(-20);
make.right.mas_equalTo(-20);
}];
你也可以将CGFloat类型的包装成NSNumber类型
[purpleView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@20);
make.left.mas_equalTo(@20);
make.bottom.mas_equalTo(@-20);
make.right.mas_equalTo(@-20);
}];
-
使用标量和结构体
// 还可以使用标量和结构体
[orangeView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(CGPointMake(0, 50));
make.size.mas_equalTo(CGSizeMake(200, 100));
}];
make.edges.mas_equalTo(lastView).insets(UIEdgeInsetsMake(5, 10, 15, 20));
(三)Edges的使用
根据我自己的理解, 使用edges如果得当的话, 会省去一些代码, 因为你甚至可以不用一一地设置top, left, bottom, right. 因为这些都可以在edges中设置好, 只要是top, left, bottom, right的属性, 都可以通过edges的insets属性来设置:
- (instancetype)init {
if (self = [super init]) {
UIView *lastView = self;
for (NSInteger i = 0; i < 10; i++) {
UIView *view = [[UIView alloc] init];
/* 添加子控件一定要放在设置约束之前, 不然约束就找不到约束对象 */
[self addSubview:view];
view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1];
view.layer.borderColor = [UIColor blackColor].CGColor;
view.layer.borderWidth = 2;
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(lastView).insets(UIEdgeInsetsMake(5, 10, 15, 20));
}];
lastView = view;
}
}
return self;
}
实现效果:
(四)自适应
在设置view大小的过程中, 我们往往不需要同时设置宽和高的具体值, 只需要设置宽高比, 但宽或者高, 一方的高度确定了, 另一方的高度也确定了.
// 布局顶部和底部的view, 使之各占据屏幕的1/2
[self addSubview:self.topView];
[self.topView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.mas_equalTo(self);
make.right.mas_equalTo(self).offset(-1);
}];
[self addSubview:self.bottomView];
[self.bottomView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.bottom.right.mas_equalTo(self);
make.top.mas_equalTo(self.topView.mas_bottom).offset(1);
make.height.mas_equalTo(self.topView);
}];
// 内部的view按照3:1的比例自适应
[self.topView addSubview:self.topInnerView];
[self.topInnerView mas_makeConstraints:^(MASConstraintMaker *make) {
// 宽是高的3倍
make.width.mas_equalTo(self.topInnerView.mas_height).multipliedBy(3);
// 宽和高不大于父控件
make.width.and.height.mas_lessThanOrEqualTo(self.topView);
// 宽和高等于父控件, 优先级低. 所以遇到上面的约束的时候, 产生的效果就是: 宽等于父控件, 高等于宽的1/3
make.width.and.height.mas_equalTo(self.topView).with.priorityLow();
make.center.mas_equalTo(self.topView);
}];
[self.bottomView addSubview:self.bottomInnerView];
[self.bottomInnerView mas_makeConstraints:^(MASConstraintMaker *make) {
// 高是宽的3倍
make.height.mas_equalTo(self.bottomInnerView.mas_width).multipliedBy(3);
// 宽和高不大于父控件
make.width.and.height.mas_lessThanOrEqualTo(self.bottomView);
// 宽和高等于父控件, 优先级低, 所以遇到上面的约束的时候, 产生的效果就是: 高等于父控件, 宽是高的1/3
make.width.and.height.mas_equalTo(self.bottomView).with.priorityLow();
make.center.mas_equalTo(self.bottomView);
}];
这段代码里涉及到了:
- multipliedBy 倍数
- priorityLow 优先级
的设置
(五)利用约束的变化制作动画
如下代码所示, self.animatableConstraints是一个放约束的数组, 数组中的约束的设置是有效的.
[greenView mas_makeConstraints:^(MASConstraintMaker *make) {
[self.animatableConstraints addObjectsFromArray:@[
make.edges.mas_equalTo(superView).insets(paddingInsets).priorityLow(),
make.bottom.mas_equalTo(blueView.mas_top).offset(-padding)
]];
make.size.mas_equalTo(redView);
make.height.mas_equalTo(blueView.mas_height);
}];
然后需要变化约束的时候, 遍历这个数组, 取出其中的约束, 赋上新值
int padding = invertedInsets ? 100 : self.padding;
UIEdgeInsets paddingInsets = UIEdgeInsetsMake(padding, padding, padding, padding);
for (MASConstraint *constraint in self.animatableConstraints) {
/**
* Modifies the NSLayoutConstraint constant,
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
*/
// 修改了NSLayoutConstraint的约束,
// 只影响MASConstraints对象中第一个控件的NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight属性
// 即上, 左, 下, 右属性
constraint.insets = paddingInsets;
}
最后将约束的更改设置成动画即可, 基本思路就是这样:
[UIView animateWithDuration:2 animations:^{
[self layoutIfNeeded];
} completion:^(BOOL finished) {
// 重复!
[self animateWithInvertedInsets:!invertedInsets];
}];
(六)消除Bug
在我们设置约束的时候, 免不了会约束冲突. 在纷繁复杂的控制台打印中无法知道自己究竟哪几个约束冲突了. 这时候我们就需要有准确的定位. Masonry为我们准备了这一切:
- 可以将解bug的key与各个view关联起来:
greenView.mas_key = @"greenView";
redView.mas_key = @"redView";
blueView.mas_key = @"blueView";
superView.mas_key = @"superView";
- 以上代码等同于
MASAttachKeys(greenView, redView, blueView, superView);
就这么一个强大的宏, 就把相应的view跟他们的名称绑定在一起了.
- 除此之外, 约束也可以绑定key
make.edges.mas_equalTo(1).key(@"conflicting constraints");
make.height.mas_greaterThanOrEqualTo(5000).key(@"constant constraints");
- 甚至
NSNumber
类型的也可以作为key
make.height.mas_equalTo(redView.mas_height).key(@31587435);
在绑定了key之后, 再出现约束冲突之后, 我们就能够很容易的发现, 究竟是哪些约束冲突了:
(七)Label的约束
这里涉及到一个新的属性, 就是基线对齐:
[self.shortLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.longLabel.mas_baseline);
make.right.mas_equalTo(self.mas_right).offset(-10);
}];
短的Label的顶部和长Label的最后一条基线对齐
在demo中是动态地设置长label的最大宽度的, 这个时候需要计算长label的最大宽度. 我们首先要super调用layoutSubviews, 拿到长和短label的frame的值, 拿到frame值之后, 计算出preferredMaxLayoutWidth
的值, 然后重新计算长短Label的frame值:
- (void)layoutSubviews {
[super layoutSubviews];
// 多行的label必须设置`preferredMaxLayoutWidth`属性的值
// 必须在调用了super之后设置, 因为只有在这个时候, 才能拿到自动布局的值
CGFloat width = CGRectGetMinX(self.shortLabel.frame) - 10;
width -= CGRectGetMinX(self.longLabel.frame);
self.longLabel.preferredMaxLayoutWidth = width;
// 需要再次调用super, 以便系统根据`preferredMaxLayoutWidth`的值再次计算frame
[super layoutSubviews];
}
但是第一次进入页面的时候会因为重新布局而闪一下, 目前还没有找到好的解决办法.
(八)更新约束
在应用中我们常常需要更新约束, 在我们需要更新约束的时候, 有两个关键的方法需要调用:
// 告诉约束他们需要更新
[self setNeedsUpdateConstraints];
// 更新约束, 以便我们可以将这种变化做成动画
[self updateConstraintsIfNeeded];
当我们调用了这两个方法之后, 就会来到
- (void)updateConstraints
这个方法, 这个方法是苹果官方推荐更新约束方法:
// 苹果官方推荐的添加和更改约束的地方 /** 在本例中, 所有的约束都写在这个方法里 */
- (void)updateConstraints {
[self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self);
/****************** -------- 设置优先级为低 -------- ******************/
/*
宽和高都是小于等于父控件的,这是大前提
然后设置约束为buttonSize的大小
当buttonSize 不断增大, 要大于父控件的宽高时, 这是不允许的
因为宽高小于等于父控件的优先级高, 所以最终的结果是宽高等于父控件k
*/
make.width.mas_equalTo(self.buttonSize.width).priorityLow();
make.height.mas_equalTo(self.buttonSize.height).priorityLow();
make.width.height.mas_lessThanOrEqualTo(self);
}];
// 根据苹果的约定, 在方法的结尾一定要调用super /** 注意: 是结尾 */
[super updateConstraints];
}
在这个方法中, 我们写需要更新的约束!
mas_updateConstraints
: 更新约束, 是以前的约束依然在, 新变化的约束将替换旧的约束, 没有变化的约束依然在, 并且不会被重复添加mas_makeConstraints
: 添加约束, 以前旧的约束不会被覆盖, 多次调用后, 会重复添加很多约束mas_remakeConstraints
: 重建约束, 这个会去除掉控件上原始的所有约束, 将新的约束添加上去
在这个demo中, 只能写更新约束:
- (void)updateConstraints {
MASAttachKeys(_growingButton, self);
/****************** -------- 这里必须写更新约束: mas_updateConstraints -------- ******************/
/****************** -------- 如果写的是重建约束 -------- ******************/
// [self.growingButton mas_remakeConstraints:^(MASConstraintMaker *make) {
// make.center.mas_equalTo(CGPointMake(0, 0));
// make.width.mas_equalTo(self.buttonSize.width).priorityLow();
// make.height.mas_equalTo(self.buttonSize.height).priorityLow();
// make.width.mas_lessThanOrEqualTo(self);
// make.height.mas_lessThanOrEqualTo(self);
// }];
/*
* 那么在:
* make.width.mas_equalTo(self.buttonSize.width).priorityLow();
* make.height.mas_equalTo(self.buttonSize.height).priorityLow();
* 这两句代码将约束的优先级设置为低,去掉这两个约束之后,button的大小变为自适应buttonTitle大小
* 当更新约束的时候, 由于之前buttonTitle大小的约束是优先级高的约束, 所以这两个优先级低的约束是加不上去的
*
*****************************************************************************************
*
* 当去掉优先级为低之后, 代码变成
* make.width.mas_equalTo(self.buttonSize.width;
* make.height.mas_equalTo(self.buttonSize.height;
* 这样代码就看似没问题了, 但是还是会有一个bug:
* 就是当button变大到比父控件还大的时候,就会和下面这两句冲突:
* make.width.mas_lessThanOrEqualTo(self);
* make.height.mas_lessThanOrEqualTo(self);
*/
/****************** -------- 如果写的是新建约束 -------- ******************/
// [self.growingButton mas_makeConstraints:^(MASConstraintMaker *make) {
// make.center.mas_equalTo(CGPointMake(0, 0));
// make.width.mas_equalTo(self.buttonSize.width).priorityLow();
// make.height.mas_equalTo(self.buttonSize.height).priorityLow();
// make.width.mas_lessThanOrEqualTo(self);
// make.height.mas_lessThanOrEqualTo(self);
// }];
/*
* 那么之前的约束通通不会消失, 依然加载控件上
* 连续点击Button按钮之后, 约束越来越多.还会出现一个问题:
* 1> 第一次要点3次按钮, 按钮才会增大
* 2> 后来要点2次按钮, 按钮才会增大
*/
/****************** -------- 所以要写更新约束 -------- ******************/
/*
* 更新约束, 之前没变的约束会一直在,不会重复添加
* 变化的约束会将之前的约束移除, 然后添加新的约束上去
*/
[self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
// make.center.mas_equalTo(self);
/* 上面这句代码还可写成 */
make.center.mas_equalTo(CGPointMake(0, 0)); // 这也可以表示button处于父控件的中心点位置
/* 下面的代码表示, 宽和高为我设置的buttonSize的大小, 但是不断增大后, button的大小依然小于等于父控件的大小 */
// make.width.mas_equalTo(self.buttonSize.width).priorityLow();
// make.height.mas_equalTo(self.buttonSize.height).priorityLow();
/* 上面的两句代码还可以简写成: */
make.size.mas_equalTo(self.buttonSize).priorityLow();
make.width.mas_lessThanOrEqualTo(self);
make.height.mas_lessThanOrEqualTo(self);
}];
// 根据苹果的规定, 必须在方法的结尾调用`super`
[super updateConstraints];
}
关于更新约束,
特别要注意的有两点:
- 在设置了约束之后, 一定要调用
super
! - 当界面一进来的时候, 就要调用
- (void)updateConstraints
这个方法, 但系统并不会自动调用这个方法, 那么我们就需要告诉系统, 我确实使用的是自动布局, 我需要界面一进来就调用这个方法, 那么我就要打开下面这个方法, 并返回YES:
/****************** -------- 在本例中, 这句代码必须retrun YES -------- ******************/
/*
* 这句代码是告诉系统, 我确实是进行时自动布局的, 这样才会一开始就调用
* `updateConstraints`方法
*/
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
(九)重建约束
重建约束将去掉控件之前所有的约束
- (void)updateConstraints {
/*
* `mas_remakeConstraints` 这个方法是去掉控件之前所有的约束
*/
[self.movingButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(100, 100));
if (self.topLeft) {
make.top.and.left.mas_equalTo(self).with.offset(10);
}else {
make.bottom.and.right.mas_equalTo(self).with.offset(-10);
}
}];
[super updateConstraints];
}
(十)ScrollView
ScrollView的约束本身是有点坑的.
- 首先, scrollView的大小是跟父控件大小相等的:
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self);
}];
- 然后scrollView内部新建一个contentView, 这个contentView实际上是跟scrollView的contentSize的大小一样的. 先设置contentView的边缘和scrollView对齐, 宽度跟scrollView相等.
[contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.scrollView);
make.width.mas_equalTo(self.scrollView);
}];
- 在contentView内部循环添加view
for (NSUInteger i = 0; i < 10; i++) {
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1];
[contentView addSubview:view];
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
[view addGestureRecognizer:singleTap];
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(lastView ? lastView.mas_bottom : @0);
make.left.mas_equalTo(0);
make.width.mas_equalTo(contentView.mas_width);
make.height.mas_equalTo(height);
}];
height += 25;
lastView = view;
}
- 最后, "封口", contentView的底部跟最后一个View的底部对齐:
[contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(lastView.mas_bottom);
}];
(十一)链式编程
Masonry最大的特点就是链式编程:
make.top.and.left.mas_equalTo(superView).insets(padding);
像and
, with
这些都是方便可读性的, 是可选的, 可要可不要.
(十二)StackView
在Masonry中有一个类似于StackView的布局
主要方法有两个:
方法一:
/**
* distribute with fixed spacing
*
* @param axisType views分布的方向
* @param fixedSpacing views之间的间距
* @param leadSpacing 第一个view与容器之间的距离
* @param tailSpacing 最后一个view与容器之间的距离
*/
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing;
方法二:
/**
* distribute with fixed item size
*
* @param axisType views分布的方向
* @param fixedItemLength 在分布方向上views的长度
* @param leadSpacing 第一个view和容器之间的距离
* @param tailSpacing 最后一个view和容器之间的距离
*/
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing;
- (instancetype)init {
if (self = [super init]) {
NSMutableArray *arr = @[].mutableCopy;
for (NSInteger i = 0; i < 4; i++) {
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1];
view.layer.borderColor = [UIColor blackColor].CGColor;
view.layer.borderWidth = 2;
[self addSubview:view];
[arr addObject:view];
}
unsigned int type = arc4random() % 4;
switch (type) {
case 0:
/*
* 参数1 : 排布方向 : 水平
* 参数2 : 控件间距 : 20
* 参数3 : 在排布方向(水平)上, 第一个控件和容器的间距(头部间距) : 5
* 参数4 : 在排布方向(水平)上, 最后一个控件和容器的间距(尾部间距) : 5
*/
[arr mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedSpacing:20 leadSpacing:5 tailSpacing:5];
// 只需要设置顶部间距, 和高度即可
[arr mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(60);
make.height.mas_equalTo(60);
}];
NSLog(@"type : %d", type);
break;
case 1:
/*
* 参数1 : 排布方向 : 垂直
* 参数2 : 控件间距 : 20
* 参数3 : 在排布方向(垂直)上, 第一个控件和容器的间距(头部间距) : 5
* 参数4 : 在排布方向(垂直)上, 最后一个控件和容器的间距(尾部间距) : 5
*/
[arr mas_distributeViewsAlongAxis:MASAxisTypeVertical withFixedSpacing:20 leadSpacing:5 tailSpacing:5];
// 只需要设置左边间距, 和宽度即可
[arr mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(0);
make.width.mas_equalTo(60);
}];
NSLog(@"type : %d", type);
break;
case 2:
/*
* 参数1 : 排布方向 : 水平
* 参数2 : 控件在排布方向上的长度(宽度) : 30
* 参数3 : 在排布方向(水平)上, 第一个控件和容器的间距(头部间距) : 200
* 参数4 : 在排布方向(水平)上, 最后一个控件和容器的间距(尾部间距) : 30
*/
[arr mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedItemLength:30 leadSpacing:200 tailSpacing:30];
// 只需要设置顶部间距, 和高度即可
[arr mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(60);
make.height.mas_equalTo(60);
}];
NSLog(@"type : %d", type);
break;
case 3:
/*
* 参数1 : 排布方向 : 垂直
* 参数2 : 控件在排布方向上的长度(高度) : 30
* 参数3 : 在排布方向(垂直)上, 第一个控件和容器的间距(头部间距) : 30
* 参数4 : 在排布方向(垂直)上, 最后一个控件和容器的间距(尾部间距) : 200
*/
[arr mas_distributeViewsAlongAxis:MASAxisTypeVertical withFixedItemLength:30 leadSpacing:30 tailSpacing:200];
// 只需要设置左边间距, 和宽度即可
[arr mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(0);
make.width.mas_equalTo(60);
}];
NSLog(@"type : %d", type);
break;
default:
break;
}
}
return self;
}
(十三)强引用约束
约束是可以作为属性的
// in public/private interface
@property (nonatomic, strong) MASConstraint *topConstraint;
...
// when making constraints
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
self.topConstraint = make.top.equalTo(superview.mas_top).with.offset(padding.top);
make.left.equalTo(superview.mas_left).with.offset(padding.left);
}];
...
// then later you can call
[self.topConstraint uninstall];
当你需要解除约束的时候, 就可以将其unstall
.