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


代碼地址如下:
http://www.demodashi.com/demo/11366.html

目錄

  • 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.
提供管理有序數據集合且可定制布局能力的對象

  • UICollectionView顯示內容時:
    • 通過dataSource獲取cell
    • 通過UICollectionViewLayout獲取layout attributes布局屬性
    • 通過對應的layout attributescell進行調整,完成布局
  • UICollectionView交互則是通過豐富的delegate方法實現

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

UICollectionView視圖

一個標准的UICollectionView視圖包括以下三個部分

  • UICollectionViewCell視圖展示單元
  • SupplementaryView追加視圖,類似我們熟悉的UITableView中的HeaderViewFooterVIew
  • DecorationView裝飾視圖

1.UICollectionView依然采用Cell重用的方式減小內存開支,所以需要我們注冊並標記,同樣,注冊分為Classnib兩類

// 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類似

// 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及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大師代發,拒絕轉載,轉載需要作者授權


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM