代碼地址以下:
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大師代發,拒絕轉載,轉載須要做者受權