转载

前两篇:
第一篇:有趣的Autolayout示例-Masonry实现
第二篇:有趣的Autolayout示例2-Masonry实现

Github地址:
https://github.com/zekunyan/AutolayoutExampleWithMasonry

Gif示例

Case 1: Parallax Header

Parallax翻译过来就是“视差”,我个人觉得就是一种“联动”的效果,在许多应用里面都能见到。当前这个例子,就是最简单的一种。

原理

原理其实就是根据UITableView当前下拉的位移值,同步改变Parallax Header的高度,即NSLayoutConstraintconstant属性,对应到Masonry里面就是重新让约束equalTo()一次。

主要步骤

主要的步骤如下:

  1. 设置UITableView背景透明。
  2. 在UITableView正下方放置一个UIImageView,作为我们的Parallax Header,设置contentModeUIViewContentModeScaleAspectFill,并加上上左右的固定约束,使其与UITableView对其,然后加上一个固定高度的约束,并在代码里面保存。
  3. 给UITableView设置一个透明的,跟Parallax Header等高的UIView,使UITableView的头部“撑开”,让后面的Parallax Header露出来。
  4. 在代码里面监听UITableView的contentOffset属性,当y小于0时,增加Parallax Header高度,使其产生联动效果。

约束示意图如下:
Parallax Header

代码

创建Parallax Header的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_parallaxHeaderView = [UIImageView new];
// 把Parallax Header放在UITableView的下面 
[self.view insertSubview:_parallaxHeaderView belowSubview:_tableView];

// 设置contentMode
_parallaxHeaderView.contentMode = UIViewContentModeScaleAspectFill;
_parallaxHeaderView.image = [UIImage imageNamed:@"parallax_header_back"];

// 添加约束
[_parallaxHeaderView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.and.right.equalTo(self.view);
    make.top.equalTo(self.mas_topLayoutGuideBottom);
    // 保存高度约束
    _parallaxHeaderHeightConstraint = make.height.equalTo(@(ParallaxHeaderHeight));
}];

监听contentOffset属性的两种方法

方法1:直接实现scrollViewDidScroll:

这种方法应该是最直接的:

1
2
3
4
5
6
7
8
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView.contentOffset.y < 0) {
        // 增加Parallax Header对应的高度,y是负数,所以减去
        _parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight - scrollView.contentOffset.y));
    } else {
        _parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight));
    }
}

方法2:KVO监听contentOffset变化

用KVO的好处就是不用要求当前类实现UITableView的delegate,对于代码的拆分有好处。

增加KVO:

1
[_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];

实现监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"contentOffset"]) {
        // 取出contentOffset值
        CGPoint contentOffset = ((NSValue *)change[NSKeyValueChangeNewKey]).CGPointValue;
        
        // 改变高度
        if (contentOffset.y < 0) {
            _parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight - contentOffset.y));
        } else {
            _parallaxHeaderHeightConstraint.equalTo(@(ParallaxHeaderHeight));
        }
    }
}

最后别忘了取消KVO:

1
2
3
- (void)dealloc {
    [_tableView removeObserver:self forKeyPath:@"contentOffset"];
}

小节

NSLayoutConstraintconstant属性非常有用,既可以做动画,也可以方便的调整现有布局,大家多多挖掘哈~

Case 2: 动态变高度的UITableViewCell

嗯,又是UITableViewCell=。=
只不过这次的是“动态改变高度”,就是类似于微信朋友圈里面的“全文”那种效果。

单纯的不定高UITableViewCell不是本例子的重点,详细请看有趣的Autolayout示例2-Masonry实现里面的Case1。

先说一点我觉得在代码设计上比较重要的地方:Cell只负责显示内容,不应该保存具体的状态信息
我们都知道,UITableViewCell是会被重用的,也就是说,不能保证UITableView里面的哪一行一定由哪一个UITableViewCell实例展示。
动态展开、收回Cell的时候,我们需要一个BOOL变量,用于保存当前Cell的展开、收回的状态。这个BOOL变量就是所谓的“状态”,这个状态应该保存在当前Cell的数据里面,如Entity、ViewModel里面。对Cell填充数据的时候,再根据这个“状态”,修改对应的约束。

主要步骤

先看看大致的步骤:

  1. 通过点击的Cell找到对应的数据Entity。
  2. 改变这一行数据Entity用于保存状态的BOOL变量的值。
  3. 让UITableView刷新这一行。
  4. 刷新的时候,Cell根据这个BOOL变量重新调整约束、填充数据,得到新的高度。
  5. 最后就是Cell的高度变化。(此时的这一行的Cell实例并不一定是之前的那个实例)

布局

为了尽量简单,例子里面的Cell只有三个子控件,第一个UILabel是标题等调试信息,第二个UILabel用来显示多行文本,最后一个UIButton用来切换展开、收回的状态。大致的布局如下:

Case2 UITableViewCell布局示意

布局的代码

标题UIlabel

1
2
3
4
5
6
7
_titleLabel = [UILabel new];
[self.contentView addSubview:_titleLabel];

[_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
    make.height.equalTo(@21);
    make.left.and.right.and.top.equalTo(self.contentView).with.insets(UIEdgeInsetsMake(4, 8, 4, 8));
}];

底部“More”按钮

1
2
3
4
5
6
7
8
9
_moreButton = [UIButton buttonWithType:UIButtonTypeSystem];
[_moreButton setTitle:@"More" forState:UIControlStateNormal];
[_moreButton addTarget:self action:@selector(switchExpandedState:) forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:_moreButton];

[_moreButton mas_makeConstraints:^(MASConstraintMaker *make) {
    make.height.equalTo(@32);
    make.left.and.right.and.bottom.equalTo(self.contentView);
}];

正文UIlabel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CGFloat preferredMaxWidth = [UIScreen mainScreen].bounds.size.width - 16;

// Content - 多行
_contentLabel = [UILabel new];
_contentLabel.numberOfLines = 0;
_contentLabel.lineBreakMode = NSLineBreakByCharWrapping;
_contentLabel.clipsToBounds = YES;
_contentLabel.preferredMaxLayoutWidth = preferredMaxWidth; // 多行时必须设置
[self.contentView addSubview:_contentLabel];

[_contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.and.right.equalTo(self.contentView).with.insets(UIEdgeInsetsMake(4, 8, 4, 8));
    make.top.equalTo(_titleLabel.mas_bottom).with.offset(4);
    make.bottom.equalTo(_moreButton.mas_top).with.offset(-4);
    // 先加上高度的限制
    _contentHeightConstraint = make.height.equalTo(@64).with.priorityHigh(); // 优先级只设置成High,比正常的高度约束低一些,防止冲突
}];

为什么要加正文UIlabel高度约束

有趣的Autolayout示例2-Masonry实现里面的Case1也讲过,获取Cell的高度的方法是systemLayoutSizeFittingSize:,如果不对正文UILabel加上高度约束,获取的高度就是根据正文的内容计算出来的,这与之前的例子里面一致。

为了使高度固定,就需要加上一个高度约束,使得systemLayoutSizeFittingSize:计算时按照这个约束去计算。

为什么正文UILabel的高度约束的优先级要调整为High

在UITableView刷新时,会先计算高度,即先调用tableView: heightForRowAtIndexPath:方法,如果高度约束为默认的1000最高的话,会产生冲突。
因为在计算的时候,我们的高度是由一个“template cell”填充内容后计算得来,这个时候的高度已经是展开以后的高度,当前的Cell还来不及调整约束(甚至不会调整,如果只用beginUpdates和endUpdates更新的话,Cell不会reload),所以降低这个高度约束的优先级,去掉冲突。

使用install和uninstall控制约束

为了能得正确高度,Cell需要根据具体的数据、状态更新约束。
这里可以使用installuninstall来控制正文UILabel高度约束是否生效。在填充Cell的数据时,就可以根据状态BOOL值来选择调用:

1
2
3
4
5
6
7
8
9
10
- (void)setEntity:(Case8DataEntity *)entity indexPath:(NSIndexPath *)indexPath {
    // 设置数据...

    // 改变约束
    if (_entity.expanded) {
        [_contentHeightConstraint uninstall];
    } else {
        [_contentHeightConstraint install];
    }
}

创建Delegate,使得Cell的事件得以回传到ViewController

在点击Cell的“More”按钮时,需要改变当前的展开收回状态BOOL值,还需要让UITableView刷新。
直接在Cell里面修改Entity数据,或者持有UITableView实例都是不恰当的,这个时候可以用Delegate模式实现。

Delegate如下:

1
2
3
@protocol Case8CellDelegate <NSObject>
- (void)case8Cell:(Case8Cell *)cell switchExpandedStateWithIndexPath:(NSIndexPath *)index;
@end

然后ViewController实现这个Protocol

1
2
3
4
5
6
7
8
9
- (void)case8Cell:(Case8Cell *)cell switchExpandedStateWithIndexPath:(NSIndexPath *)index {
    // 取出对应数据
    Case8DataEntity *case8DataEntity = _data[(NSUInteger) index.row];
    // 修改状态
    case8DataEntity.expanded = !case8DataEntity.expanded; // 切换展开还是收回
    case8DataEntity.cellHeight = 0; // 重置高度缓存
    
    // 刷新UITableView
}

Cell保存ViewController这个delegate,然后在按钮点击时回调

1
2
3
4
5
6
7
// Cellb保存delegate,注意weak
@property (weak, nonatomic) id <Case8CellDelegate> delegate;

// Cell的“More”按钮点击
- (void)switchExpandedState:(UIButton *)button {
    [_delegate case8Cell:self switchExpandedStateWithIndexPath:_indexPath];
}

刷新的方式

UITableView的刷新可以用以下几种方法:

1. reloadData

reloadData刷新,其实就是把所有Cell都刷新了一次,代价有点大,不推荐。

2. reloadRowsAtIndexPaths:withRowAnimation:

这个方法的好处就是可以指定要刷新的哪几行,而且可以指定刷新时的动画形式,一般来说用UITableViewRowAnimationFade就不错。
刷新的时候,tableView:cellForRowAtIndexPath:会被调用,原来的Cell实例会被替换。

3. beginUpdates和endUpdates

这两个方法一般都是成对使用的,在中间可以执行插入、删除等调整Cell的操作,改变Cell的高度也可以用它。
不过要注意的是,这两个方法并不会重新加载Cell,只是单纯的改变了高度,所以如果Cell原来的约束里面有高度约束这种,而又保持默认的优先级,就会产生约束冲突。

从效果上来讲,我个人觉得用reloadRowsAtIndexPaths:withRowAnimation:会更好一些~

小节

想用好Autolayout不容易啊,要仔细研究UITableView的机制=。=

Case 3: 两种方式实现等间距

等间距,也就是View之间的X或Y轴上的坐标等差,在这里我只举出水平方向上的等间距,垂直方向上一个道理。

方法1:利用透明等宽的占位View填充空白处,实现等间距

步骤很简单,就是循环创建真正要展示的View和占位View,布局如下:

利用占位View实现等间距

说明一下:

  • 占位View的宽度不能定死,这样外部的父级View宽度变化时,内部的View仍然可以保持等间距。
  • 既然占位View宽度不定,总得有个宽度的参照,这个参照就是其它的占位View,也就是说,要给占位View加上两两宽度相等的约束。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 先创建第一个占位View
UIView *lastSpaceView = [UIView new];
lastSpaceView.backgroundColor = [UIColor greenColor]; // 用绿色标出
[_containerView1 addSubview:lastSpaceView];

// 添加上左下三个约束
[lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.and.top.and.bottom.equalTo(_containerView1);
}];

// 循环创建
for (NSUInteger i = 0; i < ITEM_COUNT; i++) {
    // 创建ItemView,即真正显示内容的View
    UIView *itemView = [self getItemViewWithIndex:i];
    [_containerView1 addSubview:itemView];
    
    // 固定宽高,左边、垂直方向中心与上一个占位View对齐。
    [itemView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.height.and.width.equalTo(@(ITEM_SIZE));
        make.left.equalTo(lastSpaceView.mas_right);
        make.centerY.equalTo(_containerView1.mas_centerY);
    }];
    
    // 创建下一个占位View
    UIView *spaceView = [UIView new];
    spaceView.backgroundColor = [UIColor greenColor];
    [_containerView1 addSubview:spaceView];
    
    // 左边与当前ItemView对齐,上下与边界对齐,宽度与上一个占位View相等!
    // 但是右边的约束不能加,因为要留给下一次循环与下一个ItemView的左边界添加
    [spaceView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(itemView.mas_right).with.priorityHigh(); // 降低优先级,防止宽度不够出现约束冲突
        make.top.and.bottom.equalTo(_containerView1);
        make.width.equalTo(lastSpaceView.mas_width);
    }];
    
    // 更新
    lastSpaceView = spaceView;
}

// 为最后一个占位View添加右边约束
[lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.right.equalTo(_containerView1.mas_right);
}];

像这种重复添加View和约束,合理使用变量保存“上一次循环”创建的占位View,就可以大大简化代码,而且可以任意调整数量~

方法2:直接按比例设置multiplier

等间距,其实就是按比例,再进一步就是x坐标是按比例的。
延伸到View上,可以理解为centerX的值与父级View的宽度按比例增减。

但是,读者可以尝试一下,直接设置一个View的边界、位置属性,如centerX,等于其父级View的宽度是会报错的。

难道就没有办法了?当然不是。

始终在坐标系上考虑约束
在上一篇有趣的Autolayout示例2-Masonry实现的开头我也提到过,Autolayout最终都是体现在坐标系上,一切都会按照viewA-attribute = viewB-attribute * multiplier + constant这种公式去计算,既然centerX不能跟父级的Width宽度一起加约束,那就换一个,如父级的右边界,父级View的右边界在父级本身的参照系下的Y坐标值不就等于其宽度吗~

所以,可以按照如下方式加约束:

按比例设置multiplier实现等间距

对应的代码也会异常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 循环创建
for (NSUInteger i = 0; i < ITEM_COUNT; i++) {   
    UIView *itemView = [self getItemViewWithIndex:i];
    [_containerView2 addSubview:itemView];

    [itemView mas_makeConstraints:^(MASConstraintMaker *make) {
        // 宽高一定
        make.width.and.height.equalTo(@(ITEM_SIZE));
        // 确定Y坐标
        make.centerY.equalTo(_containerView2.mas_centerY);
        // 确定X坐标,注意分子分母都要加1
        make.centerX.equalTo(_containerView2.mas_right).multipliedBy(((CGFloat)i + 1) / ((CGFloat)ITEM_COUNT + 1));
    }];
}

小节

很多时候,灵活的使用multiplier能大大简化开发~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值