目錄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