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

目錄git

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

UICollectionView的定義

UICollectionViewUITableView同樣,是iOS中最經常使用到數據展現視圖。github

  • UICollectionView顯示內容時:編程

    • 經過dataSource獲取cell
    • 經過UICollectionViewLayout獲取layout attributes佈局屬性
    • 經過對應的layout attributescell進行調整,完成佈局
  • UICollectionView交互則是經過豐富的delegate方法實現

UICollectionView視圖

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

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

1.UICollectionView依然採用Cell重用的方式減少內存開支,因此須要咱們註冊並標記,一樣,註冊分爲Classnib兩類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及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;
        }
    }
    
}

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快速構造/拖拽重排/輪播實現介紹
Github傳送門:SPEasyCollectionView
BGM

相關文章
相關標籤/搜索