讨论点关键词
- masonry / scrollView
- lessThanOrEqualTo / greaterThanOrEqualTo
- priorityLow / priorityMedium / priorityHigh
1 序章
1.1 什么是Masonry?
Masonry是基于iOS的自适应布局支持所封装的一套第三方布局框架。该段显然不是重点,我们只需要知道,解决iOS的自适应布局问题,Masonry很好用,很强大!
1.2 处女病(强迫症)
如果你已经使用过Masonry,相信一定存在这样的状况:有些场景的布局虽然完成了期望的效果,但布局的方式总是让人感觉不满意(或说不优雅),有可能是一个子控件的布局直接使用mainScreen的宽高来进行参照,又或许是masonry在consoleLog处打印的布局冲突警告……
我们或许可以百次地说服自己说:完成需求就好,完成需求就好。但作为强迫症患者,折磨总是在所难免……
看下下面的Masonry应用Demo吧,相信可以给你带来一丝灵感的启发
2 呕心沥血选择的经典Demo
2.1 需求
如图,我们的Demo要封装这样一个视图(MTScrollContentView):
1)它(MTScrollContentView)有一个与它一样大小的滚动视图(ScrollView)
2)滚动视图上有若干个视图项
3)每个视图项含:一个大小固定的icon区域,问题标题,回答标题,会自动换行的回答详细信息。
4)滚动视图的底部有一个提示视图(截图上没有截出来)
2.2 布局
2.2.1 ScrollView在主视图的布局
/* 只是创建一个__weak的名为mainView的self变量,防止block循环引用 */
MTWeakSelf(mainView);
/* ScrollView,保持与其父视图完全重合 */
[_scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(mainView);
make.size.equalTo(mainView);
}];
- 使一个子视图和父视图完全重合的方法很多,我使用了通过size和center来实现,你当然也可以使用top+left+size或其他等价方法。
2.2.2 ItemView在"ScrollView+"上的布局
UIView *refView = nil;
MTWeakSelf(mainView);
for (int i = 0; i < _itemViewArray.count; i++) {
MTScrollContentItemView *tmpView = _itemViewArray[i];
if (0 == i) {
/* 首项,首项的纵向位置参照ScrollView */
[tmpView mas_makeConstraints:^(MASConstraintMaker *make) {
/* top参照 scrollView,因为我们期望当视图布局
* 超过scrollView的大小时,scrollView可以自动
* 扩大contentSize并支持上下滚动 */
make.top.equalTo(mainView.scrollView).offset(10);
/* left,right我们参照主视图,以防止我们的itemView
* 将scrollView在水平方向上撑大 */
make.left.equalTo(mainView).offset(15);
make.right.equalTo(mainView).offset(-15);
}];
} else {
/* 普通项目,普通项目的纵向位置参展上一个itemView的位置 */
[tmpView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(refView.mas_bottom).offset(10);
make.left.equalTo(mainView).offset(15);
make.right.equalTo(mainView).offset(-15);
}];
}
refView = tmpView;
}
-
当参照视图是一个ScrollView的时候,要多一个心眼。
因为对ScrollView的边界参照实际上是对它的的contentSize的参照。我们都知道,当ScrollView的contentSize在某个方向上大于frame时,scrollView就可以滚动了,要想一想让ScrollView可以滚动是否符合你的期望。 -
关于标题的“ScrollView+”
我想表达的是:这边,我们把ScrollView和它的父视图看作一个整体。这样,当我们想要参照ScrollView的contentSize时,对ScrollView进行参照;想参照ScrollView的frame时,对scrollView的父视图进行参照。 -
关于refView
每当一个itemView完成了布局,我们将它赋值给refView,下次循环中下个itemView进行布局时,就可以参照到上一个布局的itemView了。 -
最后一个itemView不需要参照ScrollView的底边界么?
还记得需求第4点的提示视图么?它会参照最后一个item项目,亦会参照scrollView的底边界,如下面的代码
/* Note View */
[_noteView mas_remakeConstraints:^(MASConstraintMaker *make) {
/* 参照最后一个itemView(布局位置最靠下的) */
make.top.equalTo(refView.mas_bottom);
make.left.equalTo(mainView);
make.width.equalTo(mainView);
make.height.mas_equalTo(44);
/* 参照ScrollView(的contentSize)的底边界 */
make.bottom.equalTo(mainView.scrollView);
}];
2.2.3 ItemView的子视图的布局
MTWeakSelf(mainView);
/* Icon Image View
* 高优先:距离父视图左边界10个像素,图标大小固定为90*70
* 中优先:图标举例底部大于或等于10个像素
* 低优先:图标距离顶部65个像素 */
[_iconImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(mainView).offset(10);
make.width.mas_equalTo(90).priorityHigh();
make.height.mas_equalTo(70).priorityHigh();
make.bottom.lessThanOrEqualTo(mainView).offset(-10).priorityMedium();
make.top.equalTo(mainView).offset(65).priorityLow();
}];
/* Question Label
* 高优先:距离顶部20个像素,距离左边111个像素,高25像素,距离右边6个像素 */
* 低优先:宽度大于10像素 */
[_questionLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(mainView).offset(20);
make.left.equalTo(mainView).offset(111);
make.height.mas_equalTo(25);
make.right.equalTo(mainView).offset(-6).priorityHigh();
make.width.mas_greaterThanOrEqualTo(10).priorityLow();
}];
/* Answer Title Label
* 高优先:距离问题标题底部20像素,距离左边111像素,高22像素,距离右边6像素
* 低优先:宽度大于10像素 */
[_answerTitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(mainView.questionLabel.mas_bottom).offset(20);
make.left.equalTo(mainView).offset(111);
make.height.mas_equalTo(22);
make.right.equalTo(mainView).offset(-6).priorityHigh();
make.width.mas_greaterThanOrEqualTo(10).priorityLow();
}];
/* Answer Detail Label
* 高优先:距离答案标题底部5像素,距离左边111像素,举例底部10像素,距离右边6像素
* 低优先:宽度大于10像素 */
[_answerDetailLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(mainView.answerTitleLabel.mas_bottom).offset(5);
make.left.equalTo(mainView).offset(111);
make.bottom.equalTo(mainView).offset(-10);
make.right.equalTo(mainView).offset(-6).priorityHigh();
make.width.mas_greaterThanOrEqualTo(10).priorityLow();
}];
lessThanOrEqualTo & greaterThanOrEqualTo
它们的少与多,直接参照的是offset中的数字大小。比如lessThanOrEqualTo(mainView).offset(-10)就是offset的范围为(-oo, 10]。priority布局优先级
这套布局中我们会发现priorityMedium,priorityLow这样的字眼。它们表示在遇到布局冲突的时候,我们参照哪些布局配置。为什么要设置布局优先级
当一个视图的大小进行变化的时候,其子视图的布局难免会产生冲突(即两个或多个布局条件无法同时满足),这是,很有可能出现不期望的布局错乱(Masonry亦会在consoleLog输出warning信息,这显然不优雅!)。
2.3 分析:_iconImageView的布局配置效果分析
MTWeakSelf(mainView);
/* Icon Image View
* 高优先:距离父视图左边界10个像素,图标大小固定为90*70
* 中优先:图标举例底部大于或等于10个像素
* 低优先:图标距离顶部65个像素 */
[_iconImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(mainView).offset(10);
make.width.mas_equalTo(90).priorityHigh();
make.height.mas_equalTo(70).priorityHigh();
make.bottom.lessThanOrEqualTo(mainView).offset(-10).priorityMedium();
make.top.equalTo(mainView).offset(65).priorityLow();
}];
首先看我们的布局配置:
高优先:距离父视图左边界10个像素,图标大小固定为90*70
中优先:图标距离底部大于或等于10个像素
低优先:图标距离顶部65个像素
它们在什么情况下会冲突呢?现在我们把一个itemView拉得足够高,然后慢慢缩减它的高度,看看我们的布局效果
2.3.1 itemView高度 > (65 + 70 + 10)**
此时全部一一对照我们的布局配置,发现全部满足,而且图标距离底部大于或等于,此刻取的是大于
2.3.2 itemView高度 = (65 + 70 + 10)
此时全部一一对照我们的布局配置,发现全部满足,而且图标距离底部大于或等于,此刻取的是等于
2.3.3 itemView高度 = 100
(将背景变成黄色以方便看清itemView的区域)
此刻,设定itemView的高度为100(<65 + 70 + 10),所以我们的布局条件无法全部满足了,而针对冲突的布局配置“图标高70”,“距离底边>=10”,“距离顶边65”,系统会选择打破优先级最低的布局配置:距离顶边小于65像素了。
2.3.4 itemView高度 = 50
这时候的布局效果已经不忍直视了,幸运的是,这是我强制触发的异常,这种异常状态我们是没有可能展现给用户的。(但对异常状态的展示效果,是符合我的布局期望的)
3 结语
这日,小方同学发给我一张截图,说我布局的视图Masonry报警告了!
我是很懒的……懒的届时说警告没有关系,我们展示出来的视图不会受警告的影响云云(关键是我自己看着警告也超级不爽哈哈!)……于是,剩下的办法只有解决警告!过程中发现了之前没有使用过的Masonry的priority,lessOrEqual,greateOrEqual相关参数。
拉一个独立的测试工程,分析研究下这些参数的效果,惊喜地发现之前不少布局方案都可以进一步优化。
如果你也在使用Masonry,当进行视图布局的时候若感觉有些布局需求难以达成,那么这篇文章或许会给你一些思路上的启示。静下心来,想一想你所期望的布局规则,抛开那些复杂的,奇葩的补丁布局方式。你可以用Masonry简单优雅地实现。
最后大家可以再下载Demo工程做下下面的实验:
将2.2.2节中itemView的左右参照(left/right)由参照mainView改为参照mainView.scrollView。相信可以加深我把“scrollView和它的父视图看作一个整体”这一概念思路的理解:)