前言
我们的产品突然提出一个需求,希望让用户更快地选择照片,通过滑动的方式而不是一张一张点击选择,并且给了我们一个参考对象,iPhone手机相册。
一开始准备从UITouch和响应链入手,然后根据坐标各种计算。实际操作后发现工程量太大,不好实现。后来打算用UISwipeGesture
,但是上下左右控制十分麻烦,不得不放弃。
最后github上找到一个类似的demo:Swipe to Select GridView,总算为该事件的实现打开思路。不过,原demo和我们的项目实际需求相差甚远,于是自己动手实现了该效果。我们先来看看效果:
demo
demo地址:https://pan.baidu.com/s/1nvBcN8l
核心思想
UIPanGestureRecognizer
其实一开始看原项目中是用UIPanGestureRecognizer
手势来实现滑动定位的时候还是很吃惊的,一直以为UIPanGestureRecognizer
是用来做缩放之类的手势,没想到滑动手势也能胜任。更神奇的是,如果添加到view上,而view存在UICollection,纵向滑动优先触发scrollView的上下滑动,横向滑动就触发PanGesture事件后又能纵向滑动了,不需要自己写代码控制,简直和iPhone相册一模一样。(后来根据响应链的思路想想也应该是这样。。collectionView在View的前面嘛。。)
gestureRecognizer 只要设置了最大和最小触点都是1就能识别单点滑动事件。只要响应了该手势,就能拿到UIPanGestureRecognizer
对象,通过 [gestureRecognizer locationInView:collectionView]
方法就能获得当前触点在collectionView中的位置,然后进一步比较,判断选择不选择。
- UIGestureRecognizerStateBegan
- UIGestureRecognizerStateEnded
UIPanGestureRecognizer有一个state属性,当手指触发事件的时候,state == UIGestureRecognizerStateBegan
,这时就能进行一些手势开始的操作,比如标记进入滑动状态等。当手指离开屏幕的时候,state == UIGestureRecognizerStateEnded
,这时进行手势结束操作等。其他时刻可以根据点的位置进行判断cell选中不选中。
选中 & 不选中
仔细分析iPhone相册cell选中不选中的实现可以发现规律:
- 找到第一个进入区域的cell和最后一个进入区域的cell,然后将2个cell位置之间的cell改变状态
如图,我只选中红色区域,蓝色区域也跟着选中。
- 改变的值是第一个cell变化的值
如果第一个cell变成选中,那么后面变化的cell全都是选中。如果第一个cell变成不选中,那么后面变化的cell全都不选中。
- cell先进入选中区域,然后离开选中区域,那么选中与否与cell进入选中区域之前保持一致
这一点就比较复杂了,也就是说手指滑动进入状态后,需要产生一个临时值来保存当前选中状态(tmpIsSelected)而不是最终选中状态(isSelected)。这时候就需要结合UIGestureRecognizerStateBegan
和UIGestureRecognizerStateEnded
进行判断。
大致思路如下(参考demo):
1、进入滑动状态,将所有model的isSelected的值赋值给tmpIsSelected,界面打钩不打钩的依据完全按照tmpIsSelected属性的值来显示。
2、在滑动状态中(手指滑动),cell状态改变都修改model的tmpIsSelected值。
3、结束滑动状态(手指离开),将model的tmpIsSelected的值赋值给isSelected,是否打钩都依据isSelected值显示。
区域判断
知道cell选中与不选中的规则之后,我们的任务就是找到首尾两个cell的位置,然后将之间的cell的状态改变就好。首先就是要找到第一个cell。
1、查找第一个cell
第一个cell的判断比较简单,就是看触点(x,y)坐标是否落入cell的区域内。这里需要遍历collectionView.visibleCells
,因为手势滑到的地方肯定在可视范围内,因此要找的cell肯定也在visibleCells里面。只要遍历一遍,找到点的区域在cell的frame里面的cell即可。记录下cell要改变的状态firstSelectedCellChoose
、cell的坐标firstChooseCellRect
还有cell的位置firstChooseCellIndexPath
。
2、查找第二个cell
第二个cell就要根据第一个cell的位置划分成5个区域:上侧、下侧、同行左侧、同行右侧、cell中,如上图所示。前4个区域都要根据坐标判断,然后遍历collectionView.visibleCells
,找到最后一个满足区域的cell就是第二个cell,如果不满足,就把model的tmpIsSelected
值改回isSelected
的值,达到划出区域选择恢复的效果。(这里需要注意collectionView.visibleCells并不是按上到下左到右返回的,因此还需要排个序)而在cell中这个区域不可能存在第二个cell(与第一个cell重复),因此只要把collectionView.visibleCells中所有的cell的选中状态恢复即可。具体算法可以参照demo。
自动滚动的实现
在滑动触发事件中,我们还得认为的添加2个区域以实现自动上滑和自动下滑功能。
想要做到控制同步自动上滑和自动下滑功能,我们可以设置一个参数,scrollSpeed,当scrollSpeed > 0 代表下滑,当scrollSpeed < 0代表下滑,scrollSpeed == 0代表不滑动。这样,滑动的动画就可以用公式表示出来。
- collectionView.ContentOffset.y = collectionView.ContentOffset.y + scrollSpeed;
这样做的好处是可以用一个变量就控制上下滑动,还能适当改变scrollSpeed的值加快或者减慢滑动速度。
- 注意 滑动的时候不会触发滑动手势方法,必须自己调用处理方法。
相关代码:
- (void)startScroll{
if (!startScroll ) {
return;
}
if (scrollOperationQueue.operationCount > 1) {
return ;
}
__weak typeof(self) wSelf = self;
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
if (wSelf.mainCollectionView.contentOffset.y + wSelf.mainCollectionView.frame.size.height + scrollSpeed >= wSelf.mainCollectionView.contentSize.height && scrollSpeed > 0) {
[UIView animateWithDuration:0.1 animations:^{
wSelf.mainCollectionView.contentOffset = CGPointMake(wSelf.mainCollectionView.contentOffset.x, wSelf.mainCollectionView.contentSize.height -wSelf.mainCollectionView.frame.size.height);
}];
[wSelf stopScroll];
return;
}
if (wSelf.mainCollectionView.contentOffset.y + scrollSpeed <= 0 && scrollSpeed < 0) {
[UIView animateWithDuration:0.1 animations:^{
wSelf.mainCollectionView.contentOffset = CGPointMake(wSelf.mainCollectionView.contentOffset.x, 0);
}];
[wSelf stopScroll];
return;
}
[UIView animateWithDuration:0.1 animations:^{
wSelf.mainCollectionView.contentOffset = CGPointMake(wSelf.mainCollectionView.contentOffset.x, wSelf.mainCollectionView.contentOffset.y +scrollSpeed);
}];
[wSelf dealWithPointX:scrollPoint.x pointY:scrollPoint.y];
scrollPoint = CGPointMake(scrollPoint.x, scrollPoint.y +scrollSpeed);
[wSelf performSelector:@selector(startScroll) withObject:nil afterDelay:0.1];
}];
[scrollOperationQueue addOperation:operation];
}
ps:关于滑动这块我后来又改进了下,使用UIView的动画更加流畅
总结
说了那么多,其实有很多东西只有自己去尝试后才知道是什么意思,用文字很难表达出来。
由于这个demo也是我第一次尝试,如果有什么更好方式或者效率更高的改进,欢迎在评论区提出来~
我是翻滚的牛宝宝,欢迎大家评论交流~