iOS CollectionView 列表&网格之间切换
原文地址:https://www.hlzhy.com/?p=57
前言:
最近在写一个列表界面,这个列表能够在列表和网格之间切换,这种需求算是比较常见的。本以为想我们是站在大牛的肩膀上编程,就去找了下度娘和谷哥,但是并没有找到我想要的(找到的都是不带动画的切换)。既然做不了VC战士,那就自己动手丰衣足食。在我看来,所有的视图变化都应该至少带个简单的过渡动画,当然,过度使用华丽的动画效果也会造成用户的审美疲劳。“动画有风险,使用需谨慎”。
依稀记得以前面试的时候被面试官问过这个问题,并被告知CollectionView自带有列表和网格之间切换并且带动画的API。最终找到如下方法:
/**
Summary
Changes the collection view’s layout and optionally animates the change.
Discussion
This method makes the layout change without further interaction from the user. If you choose to animate the layout change, the animation timing and parameters are controlled by the collection view.
*/
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated; // transition from one layout to another
最终效果(切换动画&动画慢放).gif
实现:
UIViewController.m
一、初始化UICollectionView
在当前控制器准备一个BOOL
值isList
,用来记录当前选择的是列表还是网格,准备两个UICollectionViewFlowLayout
对应列表和网格的布局,设置一个NOTIFIC_N_NAME
宏,将此宏作为NotificationName,稍后将以通知的方式通知Cell改变布局。并且初始化UICollectionView。
@interface ViewController ()<UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong) UICollectionView *myCollectionView;
@property (nonatomic, assign) BOOL isList;
@property (nonatomic, strong) UICollectionViewFlowLayout *gridLayout;
@property (nonatomic, strong) UICollectionViewFlowLayout *listLayout;
@end
#define NOTIFIC_N_NAME @"ViewController_changeList"
@implementation ViewController
-(UICollectionViewFlowLayout *)gridLayout{
if (!_gridLayout) {
_gridLayout = [[UICollectionViewFlowLayout alloc] init];
CGFloat width = (self.view.frame.size.width - 5) * 0.5;
_gridLayout.itemSize = CGSizeMake(width, 200 + width);
_gridLayout.minimumLineSpacing = 5;
_gridLayout.minimumInteritemSpacing = 5;
_gridLayout.sectionInset = UIEdgeInsetsZero;
}
return _gridLayout;
}
-(UICollectionViewFlowLayout *)listLayout{
if (!_listLayout) {
_listLayout = [[UICollectionViewFlowLayout alloc] init];
_listLayout.itemSize = CGSizeMake(self.view.frame.size.width, 190);
_listLayout.minimumLineSpacing = 0.5;
_listLayout.sectionInset = UIEdgeInsetsZero;
}
return _listLayout;
}
- (void)viewDidLoad {
[super viewDidLoad];
_myCollectionView = [[UICollectionView alloc]initWithFrame:self.view.bounds collectionViewLayout:self.gridLayout];
_myCollectionView.showsVerticalScrollIndicator = NO;
_myCollectionView.backgroundColor = [UIColor grayColor];
_myCollectionView.delegate = self;
_myCollectionView.dataSource = self;
[self.view addSubview:_myCollectionView];
[self.myCollectionView registerClass:[HYChangeableCell class] forCellWithReuseIdentifier:@"HYChangeableCell"];
//......
}
二、实现UICollectionViewDataSource
创建UICollectionViewCell,给cell.isList
赋值, 告诉Cell当前状态,给cell.notificationName
赋值,用以接收切换通知。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
HYChangeableCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"HYChangeableCell" forIndexPath:indexPath];
cell.isList = _isList;
cell.notificationName = NOTIFIC_N_NAME;
return cell;
}
三、点击切换按钮
通过setCollectionViewLayout:animated:
方法重新为CollectionView布局,并将animated
设为YES。但是仅仅这样是不够的,因为这样并不会触发cellForItemAtIndexPath
方法。我们还需向Cell发送通知告诉它“你需要改变布局了”。
-(void)changeListButtonClick{
_isList = !_isList;
if (_isList) {
[self.myCollectionView setCollectionViewLayout:self.listLayout animated:YES];
}else{
[self.myCollectionView setCollectionViewLayout:self.gridLayout animated:YES];
}
//[self.myCollectionView reloadData];
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFIC_N_NAME object:@(_isList)];
}
UICollectionViewCell.m
基本布局代码这里就不贴上来了,需要的请在文章最后自行下载Demo查看。
!注意:因为这里使用的是UIView动画,因为UIView动画并不会根据我们肉眼所看到的动画效果过程中来动态改变宽高,在动画开始时其宽高就已经是结束状态时的宽高。所以用Masonry给子视图布局时,约束对象尽可能的避免Cell的右边和底边。否则动画将会出现异常,如下图的TitleLabel,我们能看到在切换时title宽度是直接变短的,也造成其它Label以它为约束对象时动画异常(下面红色字体的Label,切换时会往下移位)。
一、重写layoutSubviews
通过重写layoutSubviews方法,将[super layoutSubviews]
写进UIView动画中,使Cell的切换过渡动画更平滑。
-(void)layoutSubviews{
[UIView animateWithDuration:0.3 animations:^{
[super layoutSubviews];
}];
}
二、重写setNotificationName
重写setNotificationName方法并注册观察者。实现通知方法,将通知传来的值赋值给isList
。
最后记得移除观察者!
-(void)setNotificationName:(NSString *)notificationName{
if ([_notificationName isEqualToString:notificationName]) return;
_notificationName = notificationName;
//注册通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(isListChange:) name:_notificationName object:nil];
}
-(void)isListChange:(NSNotification *)noti{
BOOL isList = [[noti object] boolValue];
[self setIsList:isList];
}
-(void)dealloc{
//移除观察者
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
三、重写setIsList
重写setIsList方法,通过判断isList
值改变子视图的布局。
代码较多,详细代码请下载Demo查看。
- 此方法内 接收到通知进入时cell的frame并不准确,此时如果需要用到self.width,则需要自行计算,例如:
-(void)setIsList:(BOOL)isList{
if (_isList == isList) return;
_isList = isList;
CGFloat width = _isList ? SCREEN_WIDTH : (SCREEN_WIDTH - 5) * 0.5;
if (_isList) {
//......
}else{
//......
}
//......
-
如使用Masonry
当布局相对简单时,约束使用mas_updateConstraints进行更新即可。当布局比较复杂,约束涉及到某控件宽,而这控件宽又是不固定的时候,可以考虑使用mas_remakeConstraints重做约束。 -
约束都设置完成后,最后调用UIView动画更新约束。如果有用frame设置的,也将设置frame代码写在UIView动画内。
!注意:如有用masonry约束关联了 用frame设置的视图,则此处需要把frame设置的视图写在前面。
-(void)setIsList:(BOOL)isList{
//......
[UIView animateWithDuration:0.3f animations:^{
self.label3.frame = frame3;
self.label4.frame = frame4;
[self.contentView layoutIfNeeded];
}];
}
Demo:
-END-
如果此文章对你有帮助,希望给个❤️。有什么问题欢迎在评论区探讨。