代码地址以下:
http://www.demodashi.com/demo/11366.htmlhtml
目录ios
UICollectionView
的定义UICollectionView
快速构建GridView网格视图UICollectionView
拖拽重排处理(iOS8.x-/iOS9.x+)UICollectionView
实现简单轮播UICollectionView
同UITableView
同样,是iOS中最经常使用到数据展现视图。
官方定义:编程
An object that manages an ordered collection of data items and presents them using customizable layouts.
提供管理有序数据集合且可定制布局能力的对象api
UICollectionView
显示内容时:
dataSource
获取cell
UICollectionViewLayout
获取layout attributes
布局属性layout attributes
对cell
进行调整,完成布局UICollectionView
交互则是经过丰富的delegate
方法实现iOS10中增长了一个新的预处理protocol UICollectionViewDataSourcePrefetching 帮助预加载数据 缓解大量数据加载带来的快速滑动时的卡顿app
一个标准的UICollectionView
视图包括如下三个部分dom
UICollectionViewCell
视图展现单元SupplementaryView
追加视图,相似咱们熟悉的UITableView
中的HeaderView
、FooterVIew
DecorationView
装饰视图1.UICollectionView
依然采用Cell
重用的方式减少内存开支,因此须要咱们注册并标记,一样,注册分为Class
及nib
两类ide
// 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
相似oop
// 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
时使用布局
这个部分使用频率极高想必你们都很是熟悉,因此笔者列出方法,再也不赘述。性能
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; } } }
若是使用UICollectionViewController,使用系统提供的默认的手势
The UICollectionViewController class provides a default gesture recognizer that you can use to rearrange items in its managed collection view. To install this gesture recognizer, set the installsStandardGestureForInteractiveMovement property of the collection view controller to YES
@property(nonatomic) BOOL installsStandardGestureForInteractiveMovement;
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之后,为咱们处理了上文中提到的手势处理、边缘检测等复杂计算,咱们只须要在合适的位置,告诉系统位置信息便可。固然,这里苹果替咱们作的动画,依然仅仅是动画。
上报位置 处理步骤以下:
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快速构造/拖拽重排/轮播实现
代码地址以下:
http://www.demodashi.com/demo/11366.html
注:本文著做权归做者,由demo大师代发,拒绝转载,转载须要做者受权