目录git
UICollectionView
的定义UICollectionView
快速构建GridView网格视图UICollectionView
拖拽重排处理(iOS8.x-/iOS9.x+)UICollectionView
实现简单轮播UICollectionView
同UITableView
同样,是iOS中最经常使用到数据展现视图。github
UICollectionView
显示内容时:编程
dataSource
获取cell
UICollectionViewLayout
获取layout attributes
布局属性layout attributes
对cell
进行调整,完成布局UICollectionView
交互则是经过丰富的delegate
方法实现一个标准的UICollectionView
视图包括如下三个部分
api
UICollectionViewCell
视图展现单元SupplementaryView
追加视图,相似咱们熟悉的UITableView
中的HeaderView
、FooterVIew
DecorationView
装饰视图1.UICollectionView
依然采用Cell
重用的方式减少内存开支,因此须要咱们注册并标记,一样,注册分为Class
及nib
两类dom
// register cell if (_cellClassName) { [_collectionView registerClass:NSClassFromString(_cellClassName) forCellWithReuseIdentifier:ReuseIdentifier]; } if (_xibName) {// xib [_collectionView registerNib:[UINib nibWithNibName:_xibName bundle:nil] forCellWithReuseIdentifier:ReuseIdentifier]; }
2.Father Apple一样将重用机制带给了SupplementaryView
,注册方法同Cell
相似ide
// UIKIT_EXTERN NSString *const UICollectionElementKindSectionHeader NS_AVAILABLE_IOS(6_0); // UIKIT_EXTERN NSString *const UICollectionElementKindSectionFooter NS_AVAILABLE_IOS(6_0); - (void)registerClass:(nullable Class)viewClass forSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier; - (void)registerNib:(nullable UINib *)nib forSupplementaryViewOfKind:(NSString *)kind withReuseIdentifier:(NSString *)identifier;
对于它尺寸的配置,一样交由Layout
处理,若是使用的是UICollectionViewFlowLayout
,能够直接经过headerReferenceSize
或footerReferenceSize
赋值
3.DecorationView
装饰视图,是咱们在自定义Custom Layout
时使用oop
这个部分使用频率极高想必你们都很是熟悉,因此笔者列出方法,再也不赘述。布局
UICollectionViewDataSource( 须要着重关注下iOS9后出现的两个新数据源方法,在下文中介绍拖拽重排时会用到他们 )性能
@required - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section; // The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath: - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; @optional - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView; // The view that is returned must be retrieved from a call to -dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath: - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; - (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(9_0); - (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath NS_AVAILABLE_IOS(9_0);
UICollectionViewDelegate学习
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0); - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0); - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; - (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath; - (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath; - (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath;
官方注释解释了交互后调用的顺序
// (when the touch begins) // 1. -collectionView:shouldHighlightItemAtIndexPath: // 2. -collectionView:didHighlightItemAtIndexPath: // // (when the touch lifts) // 3. -collectionView:shouldSelectItemAtIndexPath: or -collectionView:shouldDeselectItemAtIndexPath: // 4. -collectionView:didSelectItemAtIndexPath: or -collectionView:didDeselectItemAtIndexPath: // 5. -collectionView:didUnhighlightItemAtIndexPath:
使用代理
的方式处理数据及交互,好处是显而易见的,代码功能分工很是明确,可是也形成了必定程度上的代码书写的繁琐。因此本文会在快速构建部分,介绍如何使用Block
实现链式传参书写
不一样于UITableView
的简单布局样式,UICollectionView
提供了更增强大的布局能力,将布局样式任务分离成单独一个类管理,就是咱们初始化时必不可少UICollectionViewLayout
Custom Layout
经过UICollectionViewLayoutAttributes
,配置不一样位置Cell的诸多属性
@property (nonatomic) CGRect frame; @property (nonatomic) CGPoint center; @property (nonatomic) CGSize size; @property (nonatomic) CATransform3D transform3D; @property (nonatomic) CGRect bounds NS_AVAILABLE_IOS(7_0); @property (nonatomic) CGAffineTransform transform NS_AVAILABLE_IOS(7_0); @property (nonatomic) CGFloat alpha; @property (nonatomic) NSInteger zIndex; // default is 0
一样也能够经过Layout提供诸多行为接口
动态修改Cell的布局属性
贴心的Father Apple为了让咱们具有快速构建网格视图的能力,封装了你们都很是熟悉的线性布局UICollectionViewFlowLayout
,一样不作赘述
@property (nonatomic) CGFloat minimumLineSpacing; @property (nonatomic) CGFloat minimumInteritemSpacing; @property (nonatomic) CGSize itemSize; @property (nonatomic) CGSize estimatedItemSize NS_AVAILABLE_IOS(8_0); // defaults to CGSizeZero - setting a non-zero size enables cells that self-size via -preferredLayoutAttributesFittingAttributes: @property (nonatomic) UICollectionViewScrollDirection scrollDirection; // default is UICollectionViewScrollDirectionVertical @property (nonatomic) CGSize headerReferenceSize; @property (nonatomic) CGSize footerReferenceSize; @property (nonatomic) UIEdgeInsets sectionInset; // 悬浮Header、Footer官方支持 // Set these properties to YES to get headers that pin to the top of the screen and footers that pin to the bottom while scrolling (similar to UITableView). @property (nonatomic) BOOL sectionHeadersPinToVisibleBounds NS_AVAILABLE_IOS(9_0); @property (nonatomic) BOOL sectionFootersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);
本文中不展开讨论如何定义Custom Layout
实现诸如悬浮Header
、瀑布流、堆叠卡片等效果,鶸笔者会在近期写一篇文章详细介绍布局配置及有趣的TransitionLayout
,感兴趣的同窗能够关注一下
平常工做中,实现一个简单的网格布局CollectionView
的步骤大体分红如下几步:
UICollectionViewFlowLayout
:滑动方向、itemSize、内边距、最小行间距、最小列间距UICollectionView
:数据源、代理、注册Cell、背景颜色完成这些,代码已经写了一大堆了,若是App网格视图部分不少的话,一遍遍的写,很烦-。- 因此封装一个简单易用的UICollectionView
显得很是有必要,相信各位大佬也都作过了。
这里笔者介绍一下本身封装的CollectionView
UICollectionViewFlowLayout
知足最多见的开发需求Block
及Delegate
两种方式普通构建方式示例:
// 代码建立 SPEasyCollectionView *easyView = [[SPEasyCollectionView alloc] initWithFrame:CGRectMake(0, 20, [UIScreen mainScreen].bounds.size.width, 200)]; easyView.delegate = self; easyView.itemSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 200); easyView.scrollDirection = SPEasyScrollDirectionHorizontal; easyView.xibName = @"EasyCell"; easyView.datas = @[@"1",@"2",@"3",@"4"]; [self.view addSubview:easyView];
链式传参
// chain calls _storyboardTest.sp_cellClassName(^NSString *{ return @"TestCell"; }).sp_itemsize(^CGSize{ return CGSizeMake(100, 100); }).sp_minLineSpace(^NSInteger{ return 20; }).sp_minInterItemSpace(^NSInteger{ return 10; }).sp_scollDirection(^SPEasyScrollDirection{ return SPEasyScrollDirectionVertical; }).sp_inset(^UIEdgeInsets{ return UIEdgeInsetsMake(20, 20, 20, 20); }).sp_backgroundColor(^UIColor *{ return [UIColor colorWithRed:173/255.0 green:216/255.0 blue:230/255.0 alpha:1]; });//LightBLue #ADD8E6 173,216,230
这里分享一下链式的处理,但愿对感兴趣的同窗有所启发。其实很简单,就是Block传值
定义
// chain calls typedef SPEasyCollectionView *(^SPEasyCollectionViewItemSize)(CGSize(^)(void));
属性示例
// chain calls @property (nonatomic, readonly) SPEasyCollectionViewItemSize sp_itemsize;
属性处理示例
- (SPEasyCollectionViewItemSize)sp_itemsize{ return ^SPEasyCollectionView *(CGSize(^itemSize)()){ self.itemSize = itemSize(); return self; }; }
拖拽重排功能的实现,在iOS9以前,须要开发者本身去实现动画、边缘检测以及数据源更新,比较繁琐。iOS9以后,官方替咱们处理了相对比较复杂的前几步,只须要开发者按照正确的原则在重排完成时更新数据源便可。
拖拽重排的触发,通常都是经过长按手势触发。不管是哪一种系统环境下,都须要LongpressGestureRecognizer
的协助,因此咱们事先将它准备好
// 添加长按手势 - (void)addLongPressGestureRecognizer{ UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; longPress.minimumPressDuration = self.activeEditingModeTimeInterval?_activeEditingModeTimeInterval:2.0f; [self addGestureRecognizer:longPress]; self.longGestureRecognizer = longPress; }
说明一下手势处理的几种状态
GestureRecognizerState | 说明 |
---|---|
UIGestureRecognizerStateBegan | 手势开始 |
UIGestureRecognizerStateChanged | 手势变化 |
UIGestureRecognizerStateEnded | 手势结束 |
UIGestureRecognizerStateCancelled | 手势取消 |
UIGestureRecognizerStateFailed | 手势失败 |
UIGestureRecognizerStatePossible | 默认状态,暂未识别 |
对手势的不一样状态分别进行处理
- (void)handleEditingMode:(UILongPressGestureRecognizer *)recognizer{ switch (recognizer.state) { case UIGestureRecognizerStateBegan: { [self handleEditingMoveWhenGestureBegan:recognizer]; break; } case UIGestureRecognizerStateChanged: { [self handleEditingMoveWhenGestureChanged:recognizer]; break; } case UIGestureRecognizerStateEnded: { [self handleEditingMoveWhenGestureEnded:recognizer]; break; } default: { [self handleEditingMoveWhenGestureCanceledOrFailed:recognizer]; break; } } }
iOS8.x及之前的系统,对拖拽重排并无官方的支持。
动手以前,咱们先来理清实现思路
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.25ssnapView
边缘靠近CollectionView
的边缘必定距离时,须要开始滚动视图,与边缘交叉距离变化时,须要根据比例进行加速或减速。同时第4点中用的动画效果),也应该相应的改变速度indexPath
信息并肯定当前结束时的位置信息。同时,须要将snapView
移除,将activeCell
的显示并取消选中状态为了帮助实现边缘检测功能,笔者绘制了下图,标注UICollectionView
总体布局相关的几个重要参数,复习一下UICollectionView
的ContentSize
/frame.size/bounds.size
/edgeInset
之间的关系。由于咱们须要借助这几个参数,肯定拖拽方向及contentOffset变化范围
咱们按照上文中准备好的的手势处理方法,逐步介绍
- (void)handleEditingMoveWhenGestureBegan:(UILongPressGestureRecognizer *)recognizer{ CGPoint pressPoint = [recognizer locationInView:self.collectionView]; NSIndexPath *selectIndexPath = [self.collectionView indexPathForItemAtPoint:pressPoint]; SPBaseCell *cell = (SPBaseCell *)[_collectionView cellForItemAtIndexPath:selectIndexPath]; self.activeIndexPath = selectIndexPath; self.sourceIndexPath = selectIndexPath; self.activeCell = cell; cell.selected = YES; self.centerOffset = CGPointMake(pressPoint.x - cell.center.x, pressPoint.y - cell.center.y); self.snapViewForActiveCell = [cell snapshotViewAfterScreenUpdates:YES]; self.snapViewForActiveCell.frame = cell.frame; cell.hidden = YES; [self.collectionView addSubview:self.snapViewForActiveCell]; }
- (void)handleEditingMoveWhenGestureChanged:(UILongPressGestureRecognizer *)recognizer{ CGPoint pressPoint = [recognizer locationInView:self.collectionView]; _snapViewForActiveCell.center = CGPointMake(pressPoint.x - _centerOffset.x, pressPoint.y-_centerOffset.y); [self handleExchangeOperation];// 交换操做 [self detectEdge];// 边缘检测 }
handleExchangeOperation:处理当前snapView与visibleCells的位置关系,若是交叉超过面积的1/4,则将隐藏的activeCell同当前cell进行交换,并更新当前活动位置
- (void)handleExchangeOperation{ for (SPBaseCell *cell in self.collectionView.visibleCells) { NSIndexPath *currentIndexPath = [_collectionView indexPathForCell:cell]; if ([_collectionView indexPathForCell:cell] == self.activeIndexPath) continue; CGFloat space_x = fabs(_snapViewForActiveCell.center.x - cell.center.x); CGFloat space_y = fabs(_snapViewForActiveCell.center.y - cell.center.y); // CGFloat space = sqrtf(powf(space_x, 2) + powf(space_y, 2)); CGFloat size_x = cell.bounds.size.width; CGFloat size_y = cell.bounds.size.height; if (currentIndexPath.item > self.activeIndexPath.item) { [self.activeCells addObject:cell]; } if (space_x < size_x/2.0 && space_y < size_y/2.0) { [self handleCellExchangeWithSourceIndexPath:self.activeIndexPath destinationIndexPath:currentIndexPath]; self.activeIndexPath = currentIndexPath; } } }
handleCellExchangeWithSourceIndexPath: destinationIndexPath:对cell进行交换处理,对跨列或者跨行的交换,须要考虑cell的交换方向,咱们定义moveForward变量,做为向上(-1)/下(1)移动、向左(-1)/右(1)移动的标记,moveDirection == -1时,cell反向动画,越靠前的cell越早移动,反之moveDirection == 1时,越靠后的cell越早移动。代码中出现的changeRatio
,是咱们在边缘检测中获得的比例值,用来加速动画
- (void)handleCellExchangeWithSourceIndexPath:(NSIndexPath *)sourceIndexPath destinationIndexPath:(NSIndexPath *)destinationIndexPath{ NSInteger activeRange = destinationIndexPath.item - sourceIndexPath.item; BOOL moveForward = activeRange > 0; NSInteger originIndex = 0; NSInteger targetIndex = 0; for (NSInteger i = 1; i <= labs(activeRange); i ++) { NSInteger moveDirection = moveForward?1:-1; originIndex = sourceIndexPath.item + i*moveDirection; targetIndex = originIndex - 1*moveDirection; if (!_isEqualOrGreaterThan9_0) { CGFloat time = 0.25 - 0.11*fabs(self.changeRatio); NSLog(@"time:%f",time); [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:time]; [_collectionView moveItemAtIndexPath:[NSIndexPath indexPathForItem:originIndex inSection:sourceIndexPath.section] toIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:sourceIndexPath.section]]; [UIView commitAnimations]; } } }
detectEdge:边缘检测。定义枚举类型SPDragDirection
记录拖拽方向,咱们设置边缘检测的范围是,当snapView的边距距离最近的CollectionView显示范围边距距离小于10时,启动CADisplayLink
,按屏幕刷新率调整CollectionView的contentOffset,当手势离开这个范围时,须要将变化系数ChangeRatio
清零并销毁CADisplayLink
,减小没必要要的性能开支。同时须要更新当前snapView的位置,由于此次位置的变化并非LongPressGesture引发的,因此当手指不移动时,并不会触发手势的Changed
状态,咱们须要在修改contentOffset的位置根据视图滚动的方向去判断修改snapView.center
。这里须要注意的一点细节,在下面的代码中,咱们对baseOffset
使用了向下取整的操做,由于浮点型数据精度的问题,很容易出现1.000001^365
这种偏差增大问题。笔者在实际操做时,出现了逐渐偏移现象,因此这里特别指出,但愿各位同窗之后处理相似问题时注意
typedef NS_ENUM(NSInteger,SPDragDirection) { SPDragDirectionRight, SPDragDirectionLeft, SPDragDirectionUp, SPDragDirectionDown };
static CGFloat edgeRange = 10; static CGFloat velocityRatio = 5; - (void)detectEdge{ CGFloat baseOffset = 2; CGPoint snapView_minPoint = self.snapViewForActiveCell.frame.origin; CGFloat snapView_max_x = CGRectGetMaxX(_snapViewForActiveCell.frame); CGFloat snapView_max_y = CGRectGetMaxY(_snapViewForActiveCell.frame); // left if (snapView_minPoint.x - self.collectionView.contentOffset.x < edgeRange && self.collectionView.contentOffset.x > 0){ CGFloat intersection_x = edgeRange - (snapView_minPoint.x - self.collectionView.contentOffset.x); intersection_x = intersection_x < 2*edgeRange?2*edgeRange:intersection_x; self.changeRatio = intersection_x/(2*edgeRange); baseOffset = baseOffset * -1 - _changeRatio* baseOffset *velocityRatio; self.edgeIntersectionOffset = floorf(baseOffset); self.dragDirection = SPDragDirectionLeft; [self setupCADisplayLink]; NSLog(@"Drag left - vertical offset:%f",self.edgeIntersectionOffset); NSLog(@"CollectionView offset_X:%f",self.collectionView.contentOffset.x); } // up else if (snapView_minPoint.y - self.collectionView.contentOffset.y < edgeRange && self.collectionView.contentOffset.y > 0){ CGFloat intersection_y = edgeRange - (snapView_minPoint.y - self.collectionView.contentOffset.y); intersection_y = intersection_y > 2*edgeRange?2*edgeRange:intersection_y; self.changeRatio = intersection_y/(2*edgeRange); baseOffset = baseOffset * -1 - _changeRatio* baseOffset *velocityRatio; self.edgeIntersectionOffset = floorf(baseOffset); self.dragDirection = SPDragDirectionUp; [self setupCADisplayLink]; NSLog(@"Drag up - vertical offset:%f",self.edgeIntersectionOffset); NSLog(@"CollectionView offset_Y:%f",self.collectionView.contentOffset.y); } // right else if (snapView_max_x + edgeRange > self.collectionView.contentOffset.x + self.collectionView.bounds.size.width && self.collectionView.contentOffset.x + self.collectionView.bounds.size.width < self.collectionView.contentSize.width){ CGFloat intersection_x = edgeRange - (self.collectionView.contentOffset.x + self.collectionView.bounds.size.width - snapView_max_x); intersection_x = intersection_x > 2*edgeRange ? 2*edgeRange:intersection_x; self.changeRatio = intersection_x/(2*edgeRange); baseOffset = baseOffset + _changeRatio * baseOffset * velocityRatio; self.edgeIntersectionOffset = floorf(baseOffset); self.dragDirection = SPDragDirectionRight; [self setupCADisplayLink]; NSLog(@"Drag right - vertical offset:%f",self.edgeIntersectionOffset); NSLog(@"CollectionView offset_X:%f",self.collectionView.contentOffset.x); } // down else if (snapView_max_y + edgeRange > self.collectionView.contentOffset.y + self.collectionView.bounds.size.height && self.collectionView.contentOffset.y + self.collectionView.bounds.size.height < self.collectionView.contentSize.height){ CGFloat intersection_y = edgeRange - (self.collectionView.contentOffset.y + self.collectionView.bounds.size.height - snapView_max_y); intersection_y = intersection_y > 2*edgeRange ? 2*edgeRange:intersection_y; self.changeRatio = intersection_y/(2*edgeRange); baseOffset = baseOffset + _changeRatio* baseOffset * velocityRatio; self.edgeIntersectionOffset = floorf(baseOffset); self.dragDirection = SPDragDirectionDown; [self setupCADisplayLink]; NSLog(@"Drag down - vertical offset:%f",self.edgeIntersectionOffset); NSLog(@"CollectionView offset_Y:%f",self.collectionView.contentOffset.y); } // default else{ self.changeRatio = 0; if (self.displayLink) { [self invalidateCADisplayLink]; } } }
CADisplayLink
- (void)setupCADisplayLink{ if (self.displayLink) { return; } CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleEdgeIntersection)]; [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; self.displayLink = displayLink; } - (void)invalidateCADisplayLink{ [self.displayLink setPaused:YES]; [self.displayLink invalidate]; self.displayLink = nil; }
更新contentOffset
及snapView.center
- (void)handleEdgeIntersection{ [self handleExchangeOperation]; switch (_scrollDirection) { case SPEasyScrollDirectionHorizontal: { if (self.collectionView.contentOffset.x + self.inset.left < 0 && self.dragDirection == SPDragDirectionLeft){ return; } if (self.collectionView.contentOffset.x > self.collectionView.contentSize.width - (self.collectionView.bounds.size.width - self.inset.left) && self.dragDirection == SPDragDirectionRight){ return; } [self.collectionView setContentOffset:CGPointMake(_collectionView.contentOffset.x + self.edgeIntersectionOffset, _collectionView.contentOffset.y) animated:NO]; self.snapViewForActiveCell.center = CGPointMake(_snapViewForActiveCell.center.x + self.edgeIntersectionOffset, _snapViewForActiveCell.center.y); } break; case SPEasyScrollDirectionVertical: { if (self.collectionView.contentOffset.y + self.inset.top< 0 && self.dragDirection == SPDragDirectionUp) { return; } if (self.collectionView.contentOffset.y > self.collectionView.contentSize.height - (self.collectionView.bounds.size.height - self.inset.top) && self.dragDirection == SPDragDirectionDown) { return; } [self.collectionView setContentOffset:CGPointMake(_collectionView.contentOffset.x, _collectionView.contentOffset.y + self.edgeIntersectionOffset) animated:NO]; self.snapViewForActiveCell.center = CGPointMake(_snapViewForActiveCell.center.x, _snapViewForActiveCell.center.y + self.edgeIntersectionOffset); } break; } }
activeCell
位置上,动画结束时,移除截图并将activeCell
显示出来,销毁计时器、重置参数(呼~终于大功告成了~~ 尚未啊喂,同窗,这里得敲黑板了哈~前面但是提到了要注意动画仅仅是动画,不更新数据源的)
- (void)handleEditingMoveWhenGestureEnded:(UILongPressGestureRecognizer *)recognizer{ [self.snapViewForActiveCell removeFromSuperview]; self.activeCell.selected = NO; self.activeCell.hidden = NO; [self handleDatasourceExchangeWithSourceIndexPath:self.sourceIndexPath destinationIndexPath:self.activeIndexPath]; [self invalidateCADisplayLink]; self.edgeIntersectionOffset = 0; self.changeRatio = 0; }
由于数据源并不须要实时更新,因此咱们只须要最初位置以及最后的位置便可,交换方法复制了上面的exchangeCell方法,其实不用moveForward
参数了,全都是由于懒......
- (void)handleDatasourceExchangeWithSourceIndexPath:(NSIndexPath *)sourceIndexPath destinationIndexPath:(NSIndexPath *)destinationIndexPath{ NSMutableArray *tempArr = [self.datas mutableCopy]; NSInteger activeRange = destinationIndexPath.item - sourceIndexPath.item; BOOL moveForward = activeRange > 0; NSInteger originIndex = 0; NSInteger targetIndex = 0; for (NSInteger i = 1; i <= labs(activeRange); i ++) { NSInteger moveDirection = moveForward?1:-1; originIndex = sourceIndexPath.item + i*moveDirection; targetIndex = originIndex - 1*moveDirection; [tempArr exchangeObjectAtIndex:originIndex withObjectAtIndex:targetIndex]; } self.datas = [tempArr copy]; NSLog(@"##### %@ #####",self.datas); }
- (void)handleEditingMoveWhenGestureCanceledOrFailed:(UILongPressGestureRecognizer *)recognizer{ [UIView animateWithDuration:0.25f animations:^{ self.snapViewForActiveCell.center = self.activeCell.center; } completion:^(BOOL finished) { [self.snapViewForActiveCell removeFromSuperview]; self.activeCell.selected = NO; self.activeCell.hidden = NO; }]; [self invalidateCADisplayLink]; self.edgeIntersectionOffset = 0; self.changeRatio = 0; }
至此,咱们实现了单Section拖拽重排的UICollectionView,看一下效果,是否是感受还蛮好
Father Apple在iOS9之后,为咱们处理了上文中提到的手势处理、边缘检测等复杂计算,咱们只须要在合适的位置,告诉系统位置信息便可。固然,这里苹果替咱们作的动画,依然仅仅是动画。
上报位置 处理步骤以下:
handleEditingMoveWhenGestureBegan:
这里是上报的当前Cell的IndexPath
,并且苹果并无设置相似上文中咱们设置的centerOffset
,它是将当前触摸点,直接设置成选中cell的中心点。
[self.collectionView beginInteractiveMovementForItemAtIndexPath:selectIndexPath];
[self.collectionView updateInteractiveMovementTargetPosition:pressPoint];
[self.collectionView endInteractiveMovement];
self.activeCell.selected = NO; [self.collectionView cancelInteractiveMovement];
reloadData
方法使用- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{ BOOL canChange = self.datas.count > sourceIndexPath.item && self.datas.count > destinationIndexPath.item; if (canChange) { [self handleDatasourceExchangeWithSourceIndexPath:sourceIndexPath destinationIndexPath:destinationIndexPath]; } }
上述手势处理,能够直接合并到上文中的各手势阶段的处理中,只须要对系统版本号作判断后分状况处理便可
看一下系统的效果:
图片轮播器,几乎是如今全部App的必要组成部分了。实现轮播器的方式多种多样,这里笔者简单介绍一下,如何经过UICollectionView
实现,对更好的理解UICollectionView
及轮播器也许会有帮助( 毕竟封装进去了嘛( ͡° ͜ʖ ͡° )
思路分析:
Timer
,使用scrollToItemAtIndexPath
执行定时滚动UIPageControl
,并设置collection的cell数为_totalItemCount = _needAutoScroll?datas.count * 500:datas.count;
考虑一下几种特殊状况的处理
contentOffset
及itemSize
判断当前位置,并结合数据源data.count
计算取值位置为cell
及pageControl
当前位置赋值几处关键代码:
#pragma mark - cycle scroll actions - (void)autoScroll{ if (!_totalItemCount) return; NSInteger currentIndex = [self currentIndex]; NSInteger nextIndex = [self nextIndexWithCurrentIndex:currentIndex]; [self scroll2Index:nextIndex]; } - (void)scroll2Index:(NSInteger)index{ [_collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:index?YES:NO]; } - (NSInteger)nextIndexWithCurrentIndex:(NSInteger)index{ if (index == _totalItemCount - 1) { return 0; }else{ return index + 1; } } - (NSInteger)currentIndex{ if (_collectionView.frame.size.width == 0 || _collectionView.frame.size.height == 0) { return 0; } int index = 0; if (_layout.scrollDirection == UICollectionViewScrollDirectionHorizontal) { index = (_collectionView.contentOffset.x + _layout.itemSize.width * 0.5) / _layout.itemSize.width; } else { index = (_collectionView.contentOffset.y + _layout.itemSize.height * 0.5) / _layout.itemSize.height; } return MAX(0, index); }
- (void)setDatas:(NSArray *)datas{ _datas = datas; _totalItemCount = _needAutoScroll?datas.count * 500:datas.count; if (_needAutoScroll) { [self setupPageControl]; } [self.collectionView reloadData]; }
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{ return 1; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return _totalItemCount; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ SPBaseCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ReuseIdentifier forIndexPath:indexPath]; cell.data = self.datas[_needAutoScroll?[self getRealShownIndex:indexPath.item]:indexPath.item]; return cell; } - (NSInteger)getRealShownIndex:(NSInteger)index{ return index%_datas.count; }
代理方法,处理交互中NSTimer建立/销毁及PageControl.currentPage数据更新
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{ if (!self.datas.count) return; _pageControl.currentPage = [self getRealShownIndex:[self currentIndex]]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ if (_needAutoScroll) [self invalidateTimer]; } -(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (_needAutoScroll) [self setupTimer]; } - (void)willMoveToSuperview:(UIView *)newSuperview{ if (!newSuperview) { [self invalidateTimer]; } }
UICollectionView
做为最最最重要的视图组件之一,咱们不只须要熟练掌握,同时它dataSource/delegate+layout
,分离布局的编程思想,也很值得咱们去思考学习。
笔者简书地址:iOS-UICollectionView快速构造/拖拽重排/轮播实现介绍
笔者博客地址:iOS-UICollectionView快速构造/拖拽重排/轮播实现介绍
Github传送门:SPEasyCollectionView
BGM