确定你会使用UICollectionView?三个案列颠覆你的认
UICollectionView
是针对IOS6 以后才能使用的控件,比起UITableView
来说功能更强大,使用起来更方便,使用UICollectionView
也可以完全取代UITableView
。UICollectionView
最重要的一点就是加载设置UICollectionViewFlowLayout
。接下来就用三个demo来展示一下UICollectionView
的强大,对于iOS开发进阶还是有比较大的帮助的。
Demo下载链接:https://github.com/ZhengYaWei1992/ZWAdvanceCollectionView
第一个效果图是UICollectionViewCell
的拖动,删除,之前开发一个项目中主页面的布局自定义排版使用过这个功能,所以给整理了下。
第二个效果图是一个轻量级别的仿苹果的Cover Flow效果。主要实现是基于自定义布局的调整
第三个效果图需要你仔细看一下。重点并不是联动,重点在于效果图中右侧UICollectionView组头是随着当前界面展示的section而浮动在界面上。效果看起来简单,实际上要想实现,设计的逻辑判断不是那么简单的。在UITableView上这个效果很好实现,只需要设置UITableViewStylePlain,想进一步了解UITableView中的组头浮动相关内容请参照我之前写过的一篇文章:http://www.jianshu.com/p/3b6d9a340e59
看效果图啦,看效果图啦,看效果图啦,看效果图啦,看效果图啦。
效果图一:cell的手势拖动移动 效果图二:苹果的coverFlow效果 效果图二:浮动组头接下来就一个一个看啦。
1、UICollectionViewCell的移动。
这个很简单,但是要重点说明一点。如果将长按手势添加到cell上手势不是很灵敏。解决的方法:将长按手势添加到self.collectionView上即可.
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
NSArray *arr = @[@"手机充值", @"亲民金融", @"就业招聘", @"乡间旅游",@"乡村医疗", @"违章查询", @"生活服务", @"乡村名宿",@"新农头条"];
self.array = [NSMutableArray arrayWithArray:arr];
_longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(lonePressMoving:)];
[self.collectionView addGestureRecognizer:_longPress];
[self.view addSubview:self.collectionView];
}
长按手势实现。
- (void)lonePressMoving:(UILongPressGestureRecognizer *)longPress{
switch (longPress.state) {
case UIGestureRecognizerStateBegan:{//开始
{
//获取长按的cell
NSIndexPath *selectIndexPath = [self.collectionView indexPathForItemAtPoint:[_longPress locationInView:self.collectionView]];
CollectionViewCell *cell = (CollectionViewCell *)[self.collectionView cellForItemAtIndexPath:selectIndexPath];
//显示cell上的删除按钮
[cell.deleteButton setHidden:NO];
cell.deleteButton.tag = selectIndexPath.item;
//给当前cell上的删除按钮添加点击事件
[cell.deleteButton addTarget:self action:@selector(deleteButtonClick:) forControlEvents:UIControlEventTouchUpInside];
//设置collectionView开始移动
[_collectionView beginInteractiveMovementForItemAtIndexPath:selectIndexPath];
}
break;
}
case UIGestureRecognizerStateChanged:{//拖动中
[self.collectionView updateInteractiveMovementTargetPosition:[longPress locationInView:_longPress.view]];
break;
}
case UIGestureRecognizerStateEnded:{//结束
[self.collectionView endInteractiveMovement];
break;
}
default:
[self.collectionView cancelInteractiveMovement];
break;
}
}
删除cell代码实现。
- (void)deleteButtonClick:(UIButton *)deleteBtn{
//cell的隐藏删除设置
NSIndexPath *selectIndexPath = [self.collectionView indexPathForItemAtPoint:[_longPress locationInView:self.collectionView]];
// 找到当前的cell
CollectionViewCell *cell = (CollectionViewCell *)[self.collectionView cellForItemAtIndexPath:selectIndexPath];
cell.deleteButton.hidden = NO;
//取出源item数据
id objc = [self.array objectAtIndex:deleteBtn.tag];
//从资源数组中移除该数据
[self.array removeObject:objc];
[self.collectionView reloadData];
}
要实现的代理方法以及特别注意的事项,具体代码中有说明。重点注意第四个代理方法的实现。
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return self.array.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellId forIndexPath:indexPath];
cell.lable.text = self.array[indexPath.item];
cell.deleteButton.hidden = YES;
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
ViewController2 *vc = [[ViewController2 alloc]init];
[self.navigationController pushViewController:vc animated:YES];
}
//交换collectionView必须要实现的代理方法
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(nonnull NSIndexPath *)sourceIndexPath toIndexPath:(nonnull NSIndexPath *)destinationIndexPath{
NSIndexPath *selectIndexPath = [self.collectionView indexPathForItemAtPoint:[_longPress locationInView:self.collectionView]];
// 找到当前的cell
CollectionViewCell *cell = (CollectionViewCell *)[self.collectionView cellForItemAtIndexPath:selectIndexPath];
cell.deleteButton.hidden = YES;
/*1.存在的问题,移动是二个一个移动的效果*/
// [collectionView moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
/*2.存在的问题:只是交换而不是移动的效果*/
// [self.array exchangeObjectAtIndex:sourceIndexPath.item withObjectAtIndex:destinationIndexPath.item];
/*3.完整的解决效果*/
//取出源item数据
id objc = [self.array objectAtIndex:sourceIndexPath.item];
//从资源数组中移除该数据
[self.array removeObject:objc];
//将数据插入到资源数组中的目标位置上
[self.array insertObject:objc atIndex:destinationIndexPath.item];
[self.collectionView reloadData];
}
2、仿苹果Cover Flow效果。
2.1、自定义流式布局
这个效果的实现和第三个小何的实现核心在于自定义流式布局(UICollectionViewFlowLayout),涉及UICollectionView相关实心只要按照常规布局即可,只是在在设置布局的时候更改为自定义布局,这个自定义布局应该继承于UICollectionViewFlowLayout
。如下代码,ZWCoverFlowLayout
是继承与UICollectionViewFlowLayout
的自定义类。
ZWCoverFlowLayout *layout = [[ZWCoverFlowLayout alloc] init];
//水平方向,元素之间的最小距离
layout.minimumInteritemSpacing = 0;
//行之间的最小距离
layout.minimumLineSpacing = 20;
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
//设置元素的大小
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 64, self.view.frame.size.width, 150) collectionViewLayout:layout];
2.2、重写三个重要的系统方法。
最重要的是看自定义流式布局类的代码实现。在这个自定义类中我们要重写系统的三个方法。三个方法的作用以及参数说明如下:
设置布局属性。
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
当bounds发生变化的时候是否需要重新布局
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
//是否可以随着collectionView的滚动而变化
return YES;
}
这个方法主要是为了停止滚动的时候,让一个cell显示到正中间
返回值:当停止滚动的时候,人为停止的位置
参一:当停止滚动的时候,自然情况下根据“惯性”停留的位置
参二:每秒滚动多少个点
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
2.3、具体实现代码。
设置布局属性方法的实现。说明:每个cell唯一对应一个attribute对象,根据获取到的attribute对象我们可以直接设置每个cell的缩放布局等。
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
//1、获取cell对应的attributes对象 每个cell唯一对应一个attribute对象
NSArray *arrayAttrs = [super layoutAttributesForElementsInRect:rect];
//计算整体的中心点的x值
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.bounds.size.width * 0.5;
//2、修改attributes对象
for (UICollectionViewLayoutAttributes *attr in arrayAttrs) {
//计算每个cell和中心点的具体
CGFloat distance = ABS(attr.center.x - centerX);
//距离越大,缩放比越小,距离越小,缩放比越大
//缩放因子
CGFloat factor = 0.003;
CGFloat scale = 1 / (1 + distance * factor);
attr.transform = CGAffineTransformMakeScale(scale, scale);
}
return arrayAttrs;
}
当bounds发生变化的时候是否需要重新布局
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
//是否可以随着collectionView的滚动而变化
return YES;
}
这个方法主要是为了停止滚动的时候,让一个特定cell显示到正中间,哪一个cell距离UICollectionView的中心比较近,就将哪一个cell显示到中间。
//返回值:当停止滚动的时候,人为停止的位置
//参一:当停止滚动的时候,自然情况下根据“惯性”停留的位置
//参二:每秒滚动多少个点
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity{
//计算整体的中心点的值
CGFloat centerX = proposedContentOffset.x + self.collectionView.bounds.size.width * 0.5;
//这里不能使用contentOffset.x 因为手指一抬起来,contentOffset.x就不会再变化,按照惯性滚动的不会被计算到其中
//CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.bounds.size.width * 0.5;
//计算可视区域
CGFloat visibleX = proposedContentOffset.x;
CGFloat visibleY = proposedContentOffset.y;
CGFloat visibleW = self.collectionView.bounds.size.width;
CGFloat visibleH = self.collectionView.bounds.size.height;
//获取可视区域cell对应的attributes对象 每个cell唯一对应一个attribute对象
NSArray *arrayAttrs = [super layoutAttributesForElementsInRect:CGRectMake(visibleX, visibleY, visibleW, visibleH)];
//比较出最小的偏移
int minIdx = 0;//假设最小的下标是0
UICollectionViewLayoutAttributes *minAttr = arrayAttrs[minIdx];
//循环比较出最小的
for(int i = 1; i < arrayAttrs.count; i++){
//计算两个距离
//1、minAttr和中心点的距离
CGFloat distance1 = ABS(minAttr.center.x - centerX);
//2、计算出当前循环的attr对象和centerX的距离
UICollectionViewLayoutAttributes *obj = arrayAttrs[i];
CGFloat distance2 = obj.center.x - centerX;
//3、比较
if (distance2 < distance1) {
minIdx = i;
minAttr = obj;
}
}
//计算出最小的偏移值
CGFloat offsetX = minAttr.center.x - centerX;
return CGPointMake(offsetX + proposedContentOffset.x, proposedContentOffset.y);
}
到此就OK了,如果还想扩充更多的,只要在样式上进行简单的调整即可。如翻转、拉升、动画效果等。
3、UICollectionView的组头浮动。
其实这个效果的实现,代码量不是很多,但是在逻辑处理上还是需要考虑很多的。所以下面的代码中,我会把每一步要做的事都用语言完全描述清楚。
#######3、1 需要重写的系统方法
这个效果的实现,和第二个Cover Flow效果一样,同样需要自定义流式布局。需要重写系统的两个方法。
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
return YES:表示一旦滑动就实时调用上面这个layoutAttributesForElementsInRect:方法
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound{
return YES;
}
#######3、2 具体实现代码
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
// UICollectionViewLayoutAttributes:我称它为collectionView中的item(包括cell和header、footer这些)的《结构信息》
// 截取到父类所返回的数组(里面放的是当前屏幕所能展示的item的结构信息),并转化成不可变数组
NSMutableArray *superArray = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
// 创建存索引的数组,无符号(正整数),无序(不能通过下标取值),不可重复(重复的话会自动过滤)
NSMutableIndexSet *noneHeaderSections = [NSMutableIndexSet indexSet];
// 遍历superArray,得到一个当前屏幕中所有的section数组
for (UICollectionViewLayoutAttributes *attributes in superArray){
//如果当前的元素分类是一个cell,将cell所在的分区section加入数组,重复的话会自动过滤
if (attributes.representedElementCategory == UICollectionElementCategoryCell){
[noneHeaderSections addIndex:attributes.indexPath.section];
}
}
// 遍历superArray,将当前屏幕中拥有的header的section从数组中移除,得到一个当前屏幕中没有header的section数组
// 正常情况下,随着手指往上移,header脱离屏幕会被系统回收而cell尚在,也会触发该方法
for (UICollectionViewLayoutAttributes *attributes in superArray){
// 如果当前的元素是一个header,将header所在的section从数组中移除
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]){
[noneHeaderSections removeIndex:attributes.indexPath.section];
}
}
// 遍历当前屏幕中没有header的section数组
[noneHeaderSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) {
// 取到当前section中第一个item的indexPath
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
// 获取当前section在正常情况下已经离开屏幕的header结构信息
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
// 如果当前分区确实有因为离开屏幕而被系统回收的header
if (attributes){
// 将该header结构信息重新加入到superArray中去
[superArray addObject:attributes];
}
}];
// 遍历superArray,改变header结构信息中的参数,使它可以在当前section还没完全离开屏幕的时候一直显示
for (UICollectionViewLayoutAttributes *attributes in superArray){
// 如果当前item是header
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]){
// 得到当前header所在分区的cell的数量
NSInteger numberOfItemsInSection = [self.collectionView numberOfItemsInSection:attributes.indexPath.section];
// 得到第一个item的indexPath
NSIndexPath *firstItemIndexPath = [NSIndexPath indexPathForItem:0 inSection:attributes.indexPath.section];
// 得到最后一个item的indexPath
NSIndexPath *lastItemIndexPath = [NSIndexPath indexPathForItem:MAX(0, numberOfItemsInSection - 1) inSection:attributes.indexPath.section];
// 得到第一个item和最后一个item的结构信息
UICollectionViewLayoutAttributes *firstItemAttributes, *lastItemAttributes;
if (numberOfItemsInSection > 0){
// cell有值,则获取第一个cell和最后一个cell的结构信息
firstItemAttributes = [self layoutAttributesForItemAtIndexPath:firstItemIndexPath];
lastItemAttributes = [self layoutAttributesForItemAtIndexPath:lastItemIndexPath];
}else{
// cell没值,就新建一个UICollectionViewLayoutAttributes
firstItemAttributes = [UICollectionViewLayoutAttributes new];
// 然后模拟出在当前分区中的唯一一个cell,cell在header的下面,高度为0,还与header隔着可能存在的sectionInset的top
CGFloat y = CGRectGetMaxY(attributes.frame) + self.sectionInset.top;
firstItemAttributes.frame = CGRectMake(0, y, 0, 0);
// 因为只有一个cell,所以最后一个cell等于第一个cell
lastItemAttributes = firstItemAttributes;
}
// 获取当前header的frame
CGRect rect = attributes.frame;
// 当前的滑动距离 + 因为导航栏产生的偏移量,默认为64(如果app需求不同,需自己设置)
CGFloat offset = self.collectionView.contentOffset.y + _navHeight;
// 第一个cell的y值 - 当前header的高度 - 可能存在的sectionInset的top
CGFloat headerY = firstItemAttributes.frame.origin.y - rect.size.height - self.sectionInset.top;
// 哪个大取哪个,保证header悬停
// 针对当前header基本上都是offset更加大,针对下一个header则会是headerY大,各自处理
CGFloat maxY = MAX(offset, headerY);
// 最后一个cell的y值 + 最后一个cell的高度 + 可能存在的sectionInset的bottom - 当前header的高度
// 当当前section的footer或者下一个section的header接触到当前header的底部,计算出的headerMissingY即为有效值
CGFloat headerMissingY = CGRectGetMaxY(lastItemAttributes.frame) + self.sectionInset.bottom - rect.size.height;
// 给rect的y赋新值,因为在最后消失的临界点要跟谁消失,所以取小
rect.origin.y = MIN(maxY, headerMissingY);
// 给header的结构信息的frame重新赋值
attributes.frame = rect;
// 如果按照正常情况下,header离开屏幕被系统回收,而header的层次关系又与cell相等,如果不去理会,会出现cell在header上面的情况
// 通过打印可以知道cell的层次关系zIndex数值为0,我们可以将header的zIndex设置成1,如果不放心,也可以将它设置成非常大,这里随便填了个7
attributes.zIndex = 7;
}
}
// 转换回不可变数组,并返回
return [superArray copy];
}
// return YES:表示一旦滑动就实时调用上面这个layoutAttributesForElementsInRect:方法
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound{
return YES;
}
#######3、2 layoutAttributesForElementsInRect:
方法代码实现思路总结。
说明:superArray是截取到父类所返回的数组(里面放的是当前屏幕所能展示的item的结构信息)
1、遍历superArray,得到一个当前屏幕中所有的section数组
2、遍历superArray,将当前屏幕中拥有的header的section从数组中移除,得到一个当前屏幕中没有header的section数组
3、遍历当前屏幕中没有header的section数组,如果当前分区确实有因为离开屏幕而被系统回收的header,将该header结构信息重新加入到superArray中去。
4、遍历superArray,改变header结构信息中的参数,使它可以在当前section还没完全离开屏幕的时候一直显示。这一步里面处理的逻辑是相对比较麻烦的,要获取到同个分组中最后一个和第一个item,然后根据区分上滚和下滚控制浮动组头的显示和隐藏。
另外还有注意一个问题:
如果按照正常情况下,header离开屏幕被系统回收,而header的层次关系又与cell相等,如果不去理会,会出现cell在header上面的情况。所以这里要设置浮动组头的zIndex属性,保证其显示在最上方。
到此,针对UICollectionView的浮动组头jiu'shi'xian