iOS-UICollectionView快速構造/拖拽重排/輪播實現

代碼地址以下:
http://www.demodashi.com/demo/11366.htmlhtml

目錄ios

  • UICollectionView的定義
  • UICollectionView快速構建GridView網格視圖
  • UICollectionView拖拽重排處理(iOS8.x-/iOS9.x+)
  • UICollectionView實現簡單輪播

UICollectionView的定義

UICollectionViewUITableView同樣,是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 attributescell進行調整,完成佈局
  • UICollectionView交互則是經過豐富的delegate方法實現

iOS10中增長了一個新的預處理protocol UICollectionViewDataSourcePrefetching 幫助預加載數據 緩解大量數據加載帶來的快速滑動時的卡頓app

UICollectionView視圖

一個標準的UICollectionView視圖包括如下三個部分dom

  • UICollectionViewCell視圖展現單元
  • SupplementaryView追加視圖,相似咱們熟悉的UITableView中的HeaderViewFooterVIew
  • DecorationView裝飾視圖

1.UICollectionView依然採用Cell重用的方式減少內存開支,因此須要咱們註冊並標記,一樣,註冊分爲Classnib兩類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,能夠直接經過headerReferenceSizefooterReferenceSize賦值
3.DecorationView裝飾視圖,是咱們在自定義Custom Layout時使用佈局

UICollectionViewDataSource及UICollectionViewDelegate

這個部分使用頻率極高想必你們都很是熟悉,因此筆者列出方法,再也不贅述。性能

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實現鏈式傳參書寫

UICollectionViewLayout佈局

不一樣於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,感興趣的同窗能夠關注一下
有趣的UICollectionViewTransitionLayout

UICollectionView快速構建GridView網格視圖

平常工做中,實現一個簡單的網格佈局CollectionView的步驟大體分紅如下幾步:

  • 配置UICollectionViewFlowLayout:滑動方向、itemSize、內邊距、最小行間距、最小列間距
  • 配置UICollectionView:數據源、代理、註冊Cell、背景顏色

完成這些,代碼已經寫了一大堆了,若是App網格視圖部分不少的話,一遍遍的寫,很煩-。- 因此封裝一個簡單易用的UICollectionView顯得很是有必要,相信各位大佬也都作過了。

這裏筆者介紹一下本身封裝的CollectionView

  • 基於UIView(考慮到使用storyboard或xib快速構建時,添加UIView佔位的狀況)
  • 使用UICollectionViewFlowLayout 知足最多見的開發需求
  • 提供點擊交互方法,提供BlockDelegate兩種方式
  • 提供普通傳參鏈式傳參兩種方式
  • 支持常見輪播
  • 支持拖拽重排

普通構建方式示例:

// 代碼建立
    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;
    };
}

UICollectionView拖拽重排處理(iOS8.x-/iOS9.x+)

Strike/Freedom/Destiny有沒有膠友

拖拽重排功能的實現,在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-拖拽重排處理

iOS8.x及之前的系統,對拖拽重排並無官方的支持。

動手以前,咱們先來理清實現思路

  1. 長按Cell觸發編輯模式
  2. 手勢開始時:對當前active cell進行截圖並添加snapView在cell的位置 隱藏觸發Cell,須要記錄當前手勢觸發點距離active cell的中心點偏移量center offset
  3. 手勢移動時:根據當前觸摸點的位置及center offset更新snapView位置
  4. 手勢移動時:判斷snapViewvisibleCells的初active cell外全部cell的中心點距離,當交叉位置超過cell面積的1/4時,利用系統提供的- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;進行交換,該接口在調用時,有默認動畫,時間0.25s
  5. 手勢移動時:須要添加邊緣檢測功能,若是當前snapView邊緣靠近CollectionView的邊緣必定距離時,須要開始滾動視圖,與邊緣交叉距離變化時,須要根據比例進行加速或減速。同時第4點中用的動畫效果,也應該相應的改變速度
  6. 手勢結束時:經過系統api交換Cell時有動畫效果,並且它僅僅只是個動畫效果,因此咱們須要在手勢結束時,對數據源進行更新,這就要求咱們記錄交互開始時indexPath信息並肯定當前結束時的位置信息。同時,須要將snapView移除,將activeCell的顯示並取消選中狀態

爲了幫助實現邊緣檢測功能,筆者繪製了下圖,標註UICollectionView總體佈局相關的幾個重要參數,複習一下UICollectionViewContentSize/frame.size/bounds.size/edgeInset之間的關係。由於咱們須要藉助這幾個參數,肯定拖拽方向contentOffset變化範圍

咱們按照上文中準備好的的手勢處理方法,逐步介紹

  • handleEditingMoveWhenGestureBegan
- (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];

}
  • handleEditingMoveWhenGestureChanged
- (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;
    
}

更新contentOffsetsnapView.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;
    }
    
}
  • handleEditingMoveWhenGestureEnded
    手勢結束時,咱們應該使用動畫,將snapView的Center調整到已經交換到位的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);
}
  • handleEditingMoveWhenGestureCanceledOrFailed
    失敗或者取消手勢時,咱們直接讓snapView回去就行了嘛~必要步驟,銷燬定時器,重置參數
- (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,看一下效果,是否是感受還蠻好

iOS8.x-_demo.gif

iOS9.x+拖拽重排處理

Father Apple在iOS9之後,爲咱們處理了上文中提到的手勢處理邊緣檢測等複雜計算,咱們只須要在合適的位置,告訴系統位置信息便可。固然,這裏蘋果替咱們作的動畫,依然僅僅是動畫

上報位置 處理步驟以下:

  • handleEditingMoveWhenGestureBegan:
    這裏是上報的當前Cell的IndexPath,並且蘋果並無設置相似上文中咱們設置的centerOffset,它是將當前觸摸點,直接設置成選中cell的中心點。
[self.collectionView beginInteractiveMovementForItemAtIndexPath:selectIndexPath];
  • handleEditingMoveWhenGestureChanged:
    這裏上報的是當前觸摸點的位置
[self.collectionView updateInteractiveMovementTargetPosition:pressPoint];
  • handleEditingMoveWhenGestureEnded:
    簡單粗暴,上報結束
[self.collectionView endInteractiveMovement];
  • handleEditingMoveWhenGestureCanceledOrFailed:
    簡單粗暴,上報取消,這裏咱們須要將選中狀態清除
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];
    }
    
}

上述手勢處理,能夠直接合併到上文中的各手勢階段的處理中,只須要對系統版本號作判斷後分狀況處理便可

看一下系統的效果:

iOS9.0+_demo.gif

UICollectionView實現簡單輪播

圖片輪播器,幾乎是如今全部App的必要組成部分了。實現輪播器的方式多種多樣,這裏筆者簡單介紹一下,如何經過UICollectionView實現,對更好的理解UICollectionView輪播器也許會有幫助( 畢竟封裝進去了嘛( ͡° ͜ʖ ͡° )

cycle_pic.gif

思路分析:

  • 先肯定是否須要輪播,決定開啓定時器Timer,使用scrollToItemAtIndexPath執行定時滾動
  • 賦值數據源後,若是須要輪播,建立UIPageControl,並設置collection的cell數爲_totalItemCount = _needAutoScroll?datas.count * 500:datas.count;
  • 考慮一下幾種特殊狀況的處理
    • 當滾動到總數最後一張時,應該返回第0張,此時動畫效果設置爲NO
    • 當咱們手動滑動拖拽CollectionView時,須要中止定時器,中止拖拽時,再次開啓定時器
    • 經過contentOffsetitemSize判斷當前位置,並結合數據源data.count計算取值位置爲cellpageControl當前位置賦值

幾處關鍵代碼:

  • 滾動及位置處理
#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大師代發,拒絕轉載,轉載須要做者受權

相關文章
相關標籤/搜索