详解自动布局(Masonry)实现九宫格

以前写TimeLine中照片九宫格布局是直接计算frame,今天想用自动布局实现。

九宫格布局

使用自动布局,首先就必须知道给出了哪些条件。一般在TimeLine中照片九宫格布局给出的已知条件为:

  1. 每个单元的宽cellWidth;
  2. 每个单元的高cellHeight;
  3. 每行有几个单元numPerRow;
  4. 总共单元个数totalNum;
  5. 每个单元与边界间距viewPadding;
  6. 每个单元之间的间距viewPaddingCell。
图 1

图 1是一个九宫格,黄色区域为父视图,由已知条件可知,它的大小是由里面的单元格布局决定的,所以,只要固定住里面的单元格,父视图就会自动固定住。
下面我们来添加约束:
1.所有单元格添加高度(height)和宽度(width)约束,如图 2所示,

图 2

2.第一行相对父视图,添加top约束,如图三中紫色箭头所示,

图 3

3.非第一行添加对上一行单元的top约束,如图四红色箭头所示,

图 4

4.第一列添加对父视图的left约束,如图五墨绿色箭头所示

图 5

5.非第一列添加对上一个view的left约束,如图6深蓝色箭头所示

图 6

这时候你会发现所有单元格都固定了,但是父视图的大小却不能固定,因为还不能得出父视图的宽和高

6.右上角(第一行&最后一列)添加对父视图right约束,如图7所示,2号单元格右侧绿色箭头

图 7

7.左下角(最后一行&第一列)添加对父视图的bottom约束,如图8所示,6号单元格底部紫色箭头

图 8

talk is cheap show me the code.

doge.gif

/**
 九宫格布局(不限于九宫格,可以是N个格子),每个格子给定高(cellHeight)宽(cellWidth),
 每行格子数量(numPerRow),格子总数量(totalNum),格子与边界距离(viewPadding),格
 子之间的距离(viewPaddingCell)。

 @param cellWidth       格子宽度
 @param cellHeight      格子高度
 @param numPerRow       每行格子数量
 @param totalNum        格子总数量
 @param viewPadding     格子与边界距离
 @param viewPaddingCell 格子之间的距离
 @param superView       父视图
 */
- (void)gridWithCellWidth:(CGFloat)cellWidth
               cellHeight:(CGFloat)cellHeight
                numPerRow:(NSInteger)numPerRow
                 totalNum:(NSInteger)totalNum
              viewPadding:(CGFloat)viewPadding
          viewPaddingCell:(CGFloat)viewPaddingCell
                superView:(UIView *)superView

{
    
    __block UILabel *lastView = nil;// 创建一个空view 代表上一个view
    __block UILabel *lastRowView;// 创建一个空view 代表上一行view
    
    __block NSInteger lastRowNo = 0;//上一行的行号
    NSMutableArray *cells = [[NSMutableArray alloc] init];
    
    
    for (int i = 0; i < totalNum; i++) {
        
        UILabel *aLabel = [UILabel new];
        aLabel.text = [NSString stringWithFormat:@"%d",i];
        [superView addSubview:aLabel];
        aLabel.backgroundColor = [UIColor colorWithHue:(arc4random() % 256 / 256.0 ) saturation:( arc4random() % 128 / 256.0 ) + 0.5
                                          brightness:( arc4random() % 128 / 256.0 ) + 0.5 alpha:1.0];
        [cells addObject:aLabel];
    }
    
    // 循环创建view
    for (int i = 0; i < cells.count; i++)
    {
        
        UILabel *lb = cells[i];

        
        BOOL isFirstRow = [self isFirstRowWithIndex:i numOfRow:numPerRow];
        BOOL isFirstCol = [self isFirstColumnWithIndex:i numOfRow:numPerRow];
        
        BOOL isLastCol = [self isLastColumnWithIndex:i numOfRow:numPerRow totalNum:totalNum];
        BOOL isLastRow = [self isLastRowWithIndex:i numOfRow:numPerRow totalNum:totalNum];
        
        NSInteger curRowNo = i/numPerRow;
        if (curRowNo != lastRowNo)
        {//如果当前行与上一个view行不等,说明换行了
            lastRowView = lastView;
            lastRowNo = curRowNo;
        }
        
        // 添加约束
        [lb mas_makeConstraints:^(MASConstraintMaker *make) {
            make.width.equalTo(@(cellWidth));
            make.height.equalTo(@(cellHeight));
            
            if (isFirstRow)
            {
                make.top.equalTo(superView.mas_top).with.offset(viewPadding);
            }
            else
            {
                if (lastRowView)
                {
                    make.top.equalTo(lastRowView.mas_bottom).with.offset(viewPaddingCell);
                }
            }
            
            if (isFirstCol)
            {
                make.left.equalTo(superView.mas_left).with.offset(viewPadding);
            }
            else
            {
                if (lastView)
                {
                    make.left.equalTo(lastView.mas_right).with.offset(viewPaddingCell);
                }
            }
            
            if (isFirstRow && isLastCol)
            {
                make.right.equalTo(superView.mas_right).with.offset(-viewPadding);
            }
            
            if (isLastRow && isFirstCol)
            {
                make.bottom.equalTo(superView.mas_bottom).with.offset(-viewPadding);
            }
            
        }];
        
        
        
        // 每次循环结束 此次的View为下次约束的基准
        lastView = lb;
    }
}

代码中有一些判断,比如是否为第一行,

/**
 是否第一行

 @param index    当前下标
 @param numOfRow 每行个数

 @return YES OR NO
 */
- (BOOL)isFirstRowWithIndex:(NSInteger)index numOfRow:(NSInteger)numOfRow
{
    if (numOfRow != 0)
    {
        return index/numOfRow == 0;
    }
    return NO;
}

是否为第一列,

/**
 是否第一列
 
 @param index    当前下标
 @param numOfRow 每行个数
 
 @return YES OR NO
 */
- (BOOL)isFirstColumnWithIndex:(NSInteger)index numOfRow:(NSInteger)numOfRow
{
    if (numOfRow != 0)
    {
        return index%numOfRow == 0;
    }
    return NO;
}

是否为最后一行,

/**
 是否最后一行
 
 @param index    当前下标
 @param numOfRow 每行个数
 
 @return YES OR NO
 */
- (BOOL)isLastRowWithIndex:(NSInteger)index numOfRow:(NSInteger)numOfRow totalNum:(NSInteger)totalNum
{
    NSInteger totalRow = ceil(totalNum/((CGFloat)numOfRow));//总行数
    
    if (numOfRow != 0)
    {
        return index/numOfRow == totalRow - 1;
    }
    return NO;
}

是否为最后一列

/**
 是否最后一列
 
 @param index    当前下标
 @param numOfRow 每行个数
 
 @return YES OR NO
 */
- (BOOL)isLastColumnWithIndex:(NSInteger)index numOfRow:(NSInteger)numOfRow totalNum:(NSInteger)totalNum
{
    if (numOfRow != 0)
    {
        if (totalNum < numOfRow)
        {//总数小于每行最大个数时,如果index是最后一个,那么也是最后一列
            return index == totalNum-1;
        }
        return index%numOfRow == numOfRow - 1;
    }
    return NO;
}

注意:这里有个地方要注意,当你的单元格总数(totalNum )小于每行个数 (numOfRow),比如总共有2个单元格,每行排三个,那么最后一个即为最后一列。

上面这四个判断在很多地方都可以用到,可以记下备用🙂。

然后是上一行的view判断也需要注意。

其实这不单单只是九宫格布局,N个单元格布局也是可以的,感兴趣的小伙胖可以自行测试(好吧,估计你从我写的方法名已经看出来了,每行个数和总个数我都没有写死😂)。

另一种九宫格

这里的九宫格布局是子视图固定,而父视图由子视图决定,还有另一种情况:父视图高宽固定,子视图与父视图边界距离给定,子视图间距给定。
知道怎么布局吗?可以先思考一下。
|
|
|
|
|
|
好吧,揭晓答案:只要按照第一种九宫格前5个步骤来添加约束即可,去掉最后两步。

总结

一开始我只是想布局一个九宫格,但是后来又想,如果需求扩展到了N个单元,该如何实现呢,我的办法是从九宫格开始,由小及大来推导,然后就是要知道自动布局需要添加哪些约束,能够完整的固定视图,不能多,也不要少,这是很重要的。

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

推荐阅读更多精彩内容