更优雅地使用Masonry(含Demo)

讨论点关键词

  • 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 需求

image.png

如图,我们的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的子视图的布局

image.png
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的布局配置效果分析

image.png
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)**

image.png

此时全部一一对照我们的布局配置,发现全部满足,而且图标距离底部大于或等于,此刻取的是大于

2.3.2 itemView高度 = (65 + 70 + 10)

image.png

此时全部一一对照我们的布局配置,发现全部满足,而且图标距离底部大于或等于,此刻取的是等于

2.3.3 itemView高度 = 100

(将背景变成黄色以方便看清itemView的区域)

image.png

此刻,设定itemView的高度为100(<65 + 70 + 10),所以我们的布局条件无法全部满足了,而针对冲突的布局配置“图标高70”,“距离底边>=10”,“距离顶边65”,系统会选择打破优先级最低的布局配置:距离顶边小于65像素了

2.3.4 itemView高度 = 50

image.png

这时候的布局效果已经不忍直视了,幸运的是,这是我强制触发的异常,这种异常状态我们是没有可能展现给用户的。(但对异常状态的展示效果,是符合我的布局期望的)

3 结语

这日,小方同学发给我一张截图,说我布局的视图Masonry报警告了!
我是很懒的……懒的届时说警告没有关系,我们展示出来的视图不会受警告的影响云云(关键是我自己看着警告也超级不爽哈哈!)……于是,剩下的办法只有解决警告!过程中发现了之前没有使用过的Masonry的priority,lessOrEqual,greateOrEqual相关参数。

拉一个独立的测试工程,分析研究下这些参数的效果,惊喜地发现之前不少布局方案都可以进一步优化。

如果你也在使用Masonry,当进行视图布局的时候若感觉有些布局需求难以达成,那么这篇文章或许会给你一些思路上的启示。静下心来,想一想你所期望的布局规则,抛开那些复杂的,奇葩的补丁布局方式。你可以用Masonry简单优雅地实现。

最后大家可以再下载Demo工程做下下面的实验:
将2.2.2节中itemView的左右参照(left/right)由参照mainView改为参照mainView.scrollView。相信可以加深我把“scrollView和它的父视图看作一个整体”这一概念思路的理解:)

demo工程地址
https://github.com/chrisYooh/MasonryTest.git

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,088评论 4 62
  • 刚刚过了端午节啦!祝大家粽子节快乐! 这是第一个没有和父母一起过的端午节; 也是第一回,离家那么久。不知道简书里...
    是阿卷啊阅读 360评论 2 2
  • 《发条橙》可以以主角亚历克斯的经历分成两节。 第一节,亚历克斯意气风发,四处破坏,在罪恶中畅意游走。放在过去的江湖...
    周周就是周周阅读 656评论 0 1
  • 傍晚下去,天已经黑了,还刮着簌簌的风。我带宝儿下去,不由得裹紧了衣服。树叶被吹得刷刷作响,在枝头晃来晃去,儿子说,...
    清泠韵阅读 341评论 0 1