目录
UICollectionView
的定义UICollectionView
快速构建GridView网格视图UICollectionView
拖拽重排处理(iOS8.x-/iOS9.x+)UICollectionView
实现简单轮播
UICollectionView的定义
UICollectionView
同UITableView
一样,是iOS中最常用到数据展示视图。
UICollectionView
显示内容时:- 通过
dataSource
获取cell
- 通过
UICollectionViewLayout
获取layout attributes
布局属性 - 通过对应的
layout attributes
对cell
进行调整,完成布局
- 通过
UICollectionView
交互则是通过丰富的delegate
方法实现
UICollectionView视图
一个标准的UICollectionView
视图包括以下三个部分
UICollectionViewCell
视图展示单元SupplementaryView
追加视图,类似我们熟悉的UITableView
中的HeaderView
、FooterVIew
DecorationView
装饰视图
1.UICollectionView
依然采用Cell
重用的方式减小内存开支,所以需要我们注册并标记,同样,注册分为Class
及nib
两类
1 | // register cell |
2.Father Apple同样将重用机制带给了SupplementaryView
,注册方法同Cell
类似
1 | // UIKIT_EXTERN NSString *const UICollectionElementKindSectionHeader NS_AVAILABLE_IOS(6_0); |
对于它尺寸的配置,同样交由Layout
处理,如果使用的是UICollectionViewFlowLayout
,可以直接通过headerReferenceSize
或footerReferenceSize
赋值
3.DecorationView
装饰视图,是我们在自定义Custom Layout
时使用
UICollectionViewDataSource及UICollectionViewDelegate
这个部分使用频率极高想必大家都非常熟悉,所以笔者列出方法,不再赘述。
UICollectionViewDataSource(* 需要着重关注下iOS9后出现的两个新数据源方法,在下文中介绍拖拽重排时会用到他们 *)
1 | @required |
UICollectionViewDelegate
1 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath; |
官方注释解释了交互后调用的顺序
1 | // (when the touch begins) |
使用代理
的方式处理数据及交互,好处是显而易见的,代码功能分工非常明确,但是也造成了一定程度上的代码书写的繁琐。所以本文会在快速构建部分,介绍如何使用Block
实现链式传参书写
UICollectionViewLayout布局
不同于UITableView
的简单布局样式,UICollectionView
提供了更加强大的布局能力,将布局样式任务分离成单独一个类管理,就是我们初始化时必不可少UICollectionViewLayout
Custom Layout
通过UICollectionViewLayoutAttributes
,配置不同位置Cell的诸多属性
1 | @property (nonatomic) CGRect frame; |
同样也可以通过Layout提供诸多行为接口
动态修改Cell的布局属性
贴心的Father Apple为了让我们具备快速构建网格视图的能力,封装了大家都非常熟悉的线性布局UICollectionViewFlowLayout
,同样不做赘述
1 | @property (nonatomic) CGFloat minimumLineSpacing; |
本文中不展开讨论如何定义Custom Layout
实现诸如悬浮Header
、瀑布流、堆叠卡片等效果,鶸笔者会在近期写一篇文章详细介绍布局配置及有趣的TransitionLayout
,感兴趣的同学可以关注一下
UICollectionView快速构建GridView网格视图
日常工作中,实现一个简单的网格布局CollectionView
的步骤大致分成以下几步:
- 配置
UICollectionViewFlowLayout
:滑动方向、itemSize、内边距、最小行间距、最小列间距 - 配置
UICollectionView
:数据源、代理、注册Cell、背景颜色
完成这些,代码已经写了一大堆了,如果App网格视图部分很多的话,一遍遍的写,很烦-。- 所以封装一个简单易用的UICollectionView
显得非常有必要,相信各位大佬也都做过了。
这里笔者介绍一下自己封装的CollectionView
- 基于UIView(考虑到使用storyboard或xib快速构建时,添加UIView占位的情况)
- 使用
UICollectionViewFlowLayout
满足最常见的开发需求 - 提供点击交互方法,提供
Block
及Delegate
两种方式 - 提供普通传参及链式传参两种方式
- 支持常见轮播
- 支持拖拽重排
普通构建方式示例:
1 | // 代码创建 |
链式传参
1 | // chain calls |
这里分享一下链式的处理,希望对感兴趣的同学有所启发。其实很简单,就是Block传值
定义
1 | // chain calls |
属性示例
1 | // chain calls |
属性处理示例
1 | - (SPEasyCollectionViewItemSize)sp_itemsize{ |
UICollectionView拖拽重排处理(iOS8.x-/iOS9.x+)
拖拽重排功能的实现,在iOS9之前,需要开发者自己去实现动画、边缘检测以及数据源更新,比较繁琐。iOS9之后,官方替我们处理了相对比较复杂的前几步,只需要开发者按照正确的原则在重排完成时更新数据源即可。
拖拽重排的触发,一般都是通过长按手势触发。无论是哪种系统环境下,都需要LongpressGestureRecognizer
的协助,所以我们事先将它准备好
1 | // 添加长按手势 |
说明一下手势处理的几种状态
GestureRecognizerState | 说明 | ||
---|---|---|---|
UIGestureRecognizerStateBegan | 手势开始 | ||
UIGestureRecognizerStateChanged | 手势变化 | ||
UIGestureRecognizerStateEnded | 手势结束 | ||
UIGestureRecognizerStateCancelled | 手势取消 | ||
UIGestureRecognizerStateFailed | 手势失败 | ||
UIGestureRecognizerStatePossible | 默认状态,暂未识别 | ||
对手势的不同状态分别进行处理 | |||
|
iOS8.x-拖拽重排处理
iOS8.x及以前的系统,对拖拽重排并没有官方的支持。
动手之前,我们先来理清实现思路
- 长按Cell触发编辑模式
- 手势开始时:对当前
active cell
进行截图并添加snapView
在cell的位置 隐藏触发Cell,需要记录当前手势触发点距离active cell
的中心点偏移量center offset
- 手势移动时:根据当前触摸点的位置及
center offset
更新snapView
位置 - 手势移动时:判断
snapView
同visibleCells
的初active cell
外所有cell的中心点距离,当交叉位置超过cell面积的1/4时,利用系统提供的- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;
进行交换,该接口在调用时,有默认动画,时间0.25s - 手势移动时:需要添加边缘检测功能,如果当前
snapView
边缘靠近CollectionView
的边缘一定距离时,需要开始滚动视图,与边缘交叉距离变化时,需要根据比例进行加速或减速。同时第4点中用的动画效果,也应该相应的改变速度 - 手势结束时:通过系统api交换Cell时有动画效果,而且它仅仅只是个动画效果,所以我们需要在手势结束时,对数据源进行更新,这就要求我们记录交互开始时
indexPath
信息并确定当前结束时的位置信息。同时,需要将snapView
移除,将activeCell
的显示并取消选中状态
为了帮助实现边缘检测功能,笔者绘制了下图,标注UICollectionView
整体布局相关的几个重要参数,复习一下UICollectionView
的ContentSize
/frame.size/bounds.size
/edgeInset
之间的关系。因为我们需要借助这几个参数,确定拖拽方向及contentOffset变化范围
我们按照上文中准备好的的手势处理方法,逐步介绍
- handleEditingMoveWhenGestureBegan
1 | - (void)handleEditingMoveWhenGestureBegan:(UILongPressGestureRecognizer *)recognizer{ |
- handleEditingMoveWhenGestureChanged
1 | - (void)handleEditingMoveWhenGestureChanged:(UILongPressGestureRecognizer *)recognizer{ |
handleExchangeOperation:处理当前snapView与visibleCells的位置关系,如果交叉超过面积的1/4,则将隐藏的activeCell同当前cell进行交换,并更新当前活动位置
1 | - (void)handleExchangeOperation{ |
handleCellExchangeWithSourceIndexPath: destinationIndexPath:对cell进行交换处理,对跨列或者跨行的交换,需要考虑cell的交换方向,我们定义moveForward变量,作为向上(-1)/下(1)移动、向左(-1)/右(1)移动的标记,moveDirection == -1时,cell反向动画,越靠前的cell越早移动,反之moveDirection == 1时,越靠后的cell越早移动。代码中出现的changeRatio
,是我们在边缘检测中得到的比例值,用来加速动画
1 | - (void)handleCellExchangeWithSourceIndexPath:(NSIndexPath *)sourceIndexPath destinationIndexPath:(NSIndexPath *)destinationIndexPath{ |
detectEdge:边缘检测。定义枚举类型SPDragDirection
记录拖拽方向,我们设置边缘检测的范围是,当snapView的边距距离最近的CollectionView显示范围边距距离小于10时,启动CADisplayLink
,按屏幕刷新率调整CollectionView的contentOffset,当手势离开这个范围时,需要将变化系数ChangeRatio
清零并销毁CADisplayLink
,减少不必要的性能开支。同时需要更新当前snapView的位置,因为这次位置的变化并不是LongPressGesture引起的,所以当手指不移动时,并不会触发手势的Changed
状态,我们需要在修改contentOffset的位置根据视图滚动的方向去判断修改snapView.center
。这里需要注意的一点细节,在下面的代码中,我们对baseOffset
使用了向下取整的操作,因为浮点型数据精度的问题,很容易出现1.000001^365
这种误差增大问题。笔者在实际操作时,出现了逐渐偏移现象,所以这里特别指出,希望各位同学以后处理类似问题时注意
1 | typedef NS_ENUM(NSInteger,SPDragDirection) { |
1 | static CGFloat edgeRange = 10; |
CADisplayLink
1 | - (void)setupCADisplayLink{ |
更新contentOffset
及snapView.center
1 | - (void)handleEdgeIntersection{ |
- handleEditingMoveWhenGestureEnded
手势结束时,我们应该使用动画,将snapView的Center调整到已经交换到位的activeCell
位置上,动画结束时,移除截图并将activeCell
显示出来,销毁计时器、重置参数
(呼终于大功告成了还没有啊喂,同学,这里得敲黑板了哈~前面可是提到了要注意动画仅仅是动画,不更新数据源的)
1 | - (void)handleEditingMoveWhenGestureEnded:(UILongPressGestureRecognizer *)recognizer{ |
因为数据源并不需要实时更新,所以我们只需要最初位置以及最后的位置即可,交换方法复制了上面的exchangeCell方法,其实不用moveForward
参数了,全都是因为懒……
1 | - (void)handleDatasourceExchangeWithSourceIndexPath:(NSIndexPath *)sourceIndexPath destinationIndexPath:(NSIndexPath *)destinationIndexPath{ |
- handleEditingMoveWhenGestureCanceledOrFailed
失败或者取消手势时,我们直接让snapView回去就好了嘛~必要步骤,销毁定时器,重置参数
1 | - (void)handleEditingMoveWhenGestureCanceledOrFailed:(UILongPressGestureRecognizer *)recognizer{ |
至此,我们实现了单Section拖拽重排的UICollectionView,看一下效果,是不是感觉还蛮好
iOS9.x+拖拽重排处理
Father Apple在iOS9以后,为我们处理了上文中提到的手势处理、边缘检测等复杂计算,我们只需要在合适的位置,告诉系统位置信息即可。当然,这里苹果替我们做的动画,依然仅仅是动画。
上报位置 处理步骤如下:
handleEditingMoveWhenGestureBegan:
这里是上报的当前Cell的IndexPath
,而且苹果并没有设置类似上文中我们设置的centerOffset
,它是将当前触摸点,直接设置成选中cell的中心点。1
[self.collectionView beginInteractiveMovementForItemAtIndexPath:selectIndexPath];
handleEditingMoveWhenGestureChanged:
这里上报的是当前触摸点的位置1
[self.collectionView updateInteractiveMovementTargetPosition:pressPoint];
handleEditingMoveWhenGestureEnded:
简单粗暴,上报结束1
[self.collectionView endInteractiveMovement];
handleEditingMoveWhenGestureCanceledOrFailed:
简单粗暴,上报取消,这里我们需要将选中状态清除1
2self.activeCell.selected = NO;
[self.collectionView cancelInteractiveMovement];系统新的数据源方法:
处理结束回调,根据交换信息,更新数据源供回调完成后系统自动调用reloadData
方法使用
1 | - (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{ |
上述手势处理,可以直接合并到上文中的各手势阶段的处理中,只需要对系统版本号做判断后分情况处理即可
看一下系统的效果:
UICollectionView实现简单轮播
图片轮播器,几乎是现在所有App的必要组成部分了。实现轮播器的方式多种多样,这里笔者简单介绍一下,如何通过UICollectionView
实现,对更好的理解UICollectionView
及轮播器也许会有帮助( 毕竟封装进去了嘛 ( ͡° ͜ʖ ͡° )
思路分析:
- 先确定是否需要轮播,决定开启定时器
Timer
,使用scrollToItemAtIndexPath
执行定时滚动 - 赋值数据源后,如果需要轮播,创建
UIPageControl
,并设置collection的cell数为_totalItemCount = _needAutoScroll?datas.count * 500:datas.count;
- 考虑一下几种特殊情况的处理
- 当滚动到总数最后一张时,应该返回第0张,此时动画效果设置为NO
- 当我们手动滑动拖拽CollectionView时,需要停止定时器,停止拖拽时,再次开启定时器
- 通过
contentOffset
及itemSize
判断当前位置,并结合数据源data.count
计算取值位置为cell
及pageControl
当前位置赋值
几处关键代码:
- 滚动及位置处理
1 | #pragma mark - cycle scroll actions |
数据源处理
数据
1 | - (void)setDatas:(NSArray *)datas{ |
- 数据源
1 | - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{ |
代理方法,处理交互中NSTimer创建/销毁及PageControl.currentPage数据更新
1 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView{ |
总结
UICollectionView
作为最最最重要的视图组件之一,我们不仅需要熟练掌握,同时它dataSource/delegate+layout
,分离布局的编程思想,也很值得我们去思考学习。
笔者简书地址:iOS-UICollectionView快速构造/拖拽重排/轮播实现介绍
Github传送门:SPEasyCollectionView