UICollectionView(一)——整體總結

weixin_33670713發表於2015-12-05

前言

這幾天有時間看了下UICollectionView的東西,才發覺它真的非常強大,很有必要好好學習學習。以前雖然用過幾次,但沒有系統的整理總結過。這兩天我為UICollectionView做一個比較全面的整理。包括基本使用自定義佈局自定義插入刪除動畫自定義轉場動畫等幾部分。好了,開始。

UICollectionView相對於UITableView可以說是青出於藍而勝於藍,它和UITableView很相似,但它要更加強大。
UITableView的佈局形式比較單一,侷限於行列表,而UICollectionView的強大之處在於把檢視佈局分離出來成為一個獨立的類,你想實現怎樣的檢視佈局,就子類化這個類並在其中實現。

UICollectionView基礎

  • UICollectionViewFlowLayout:檢視佈局物件(流檢視:一行排滿,自動排到下行),繼承自UICollectionViewLayout。
    UICollectionViewLayout有個collectionView屬性,
    所有的檢視佈局物件都繼承自UICollectionViewLayout。若我們要自定義佈局物件,我們一般繼承UICollectionViewFlowLayout就可以了。
  • 需要實現三個協議;UICollectionViewDataSource(資料來源)、UICollectionViewDelegateFlowLayout(檢視佈局)、UICollectionViewDelegate。
    可以看得出,除了檢視佈局,UICollectionView幾乎和UITableView一樣,但這也正是它的強大之處。
1.建立UICollectionView檢視
- (void)loadCollectionView
{
    _customLayout = [[CustomCollectionViewLayout alloc] init]; // 自定義的佈局物件
    _collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:_customLayout];
    _collectionView.backgroundColor = [UIColor whiteColor];
    _collectionView.dataSource = self;
    _collectionView.delegate = self;
    [self.view addSubview:_collectionView];
    
    // 註冊cell、sectionHeader、sectionFooter
    [_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:cellId];
    [_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:headerId];
    [_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:footerId];
}

需要注意的是這幾行程式碼的位置,及const的位置。(我經常搞亂)

@implementation YWViewController

// 注意const的位置
static NSString *const cellId = @"cellId";
static NSString *const headerId = @"headerId";
static NSString *const footerId = @"footerId";


- (void)viewDidLoad
{
2.實現UICollectionViewDataSource的幾個代理方法

#pragma mark ---- UICollectionViewDataSource

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return 1;
}


- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return _section0Array.count;
}


- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:cellId forIndexPath:indexPath];
    cell.backgroundColor = [UIColor purpleColor];
    
    return cell;
}

// 和UITableView類似,UICollectionView也可設定段頭段尾
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{

    if([kind isEqualToString:UICollectionElementKindSectionHeader])
    {
        UICollectionReusableView *headerView = [_collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:headerId forIndexPath:indexPath];
        if(headerView == nil)
        {
            headerView = [[UICollectionReusableView alloc] init];
        }
        headerView.backgroundColor = [UIColor grayColor];
        
        return headerView;
    }
    else if([kind isEqualToString:UICollectionElementKindSectionFooter])
    {
        UICollectionReusableView *footerView = [_collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:footerId forIndexPath:indexPath];
        if(footerView == nil)
        {
            footerView = [[UICollectionReusableView alloc] init];
        }
        footerView.backgroundColor = [UIColor lightGrayColor];
        
        return footerView;
    }
    
    return nil;
}

- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}


- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath
{
    
}




#pragma mark ---- UICollectionViewDelegateFlowLayout

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return (CGSize){cellWidth,cellWidth};
}


- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
    return UIEdgeInsetsMake(5, 5, 5, 5);
}


- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
    return 5.f;
}


- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
    return 5.f;
}


- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
{
    return (CGSize){ScreenWidth,44};
}


- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section
{
    return (CGSize){ScreenWidth,22};
}




#pragma mark ---- UICollectionViewDelegate

- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}

// 點選高亮
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    cell.backgroundColor = [UIColor greenColor];
}


// 選中某item
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    
}


// 長按某item,彈出copy和paste的選單
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}

// 使copy和paste有效
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender
{
    if ([NSStringFromSelector(action) isEqualToString:@"copy:"] || [NSStringFromSelector(action) isEqualToString:@"paste:"])
    {
        return YES;
    }
    
    return NO;
}

//
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender
{
    if([NSStringFromSelector(action) isEqualToString:@"copy:"])
    {
//      NSLog(@"-------------執行拷貝-------------");
        [_collectionView performBatchUpdates:^{
            [_section0Array removeObjectAtIndex:indexPath.row];
            [_collectionView deleteItemsAtIndexPaths:@[indexPath]];
        } completion:nil];
    }
    else if([NSStringFromSelector(action) isEqualToString:@"paste:"])
    {
        NSLog(@"-------------執行貼上-------------");
    }
}

UICollectionView自定義佈局

要自定義UICollectionView佈局,就要子類化UICollectionViewLayout,然後重寫它的一些方法以達到我們自定義佈局的需求。下來我們來看看UICollectionViewLayout類裡一些比較重要的方法:

  • - (void)prepareLayout;為layout顯示做準備工作,你可以在該方法裡設定一些屬性。
  • - (CGSize)collectionViewContentSize;返回layout的size。
  • *- (NSArray )layoutAttributesForElementsInRect:(CGRect)rect;返回在collectionView的可見範圍內(bounds)所有item對應的layoutAttrure物件裝成的陣列。collectionView的每個item都對應一個專門的UICollectionViewLayoutAttributes型別的物件來表示該item的一些屬性,比如bounds,size,transform,alpha等。
  • **- (UICollectionViewLayoutAttributes )layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath;傳入indexPath,返回該indexPath對應的layoutAtture物件。
  • **- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds; **噹噹前layout的佈局發生變動時,是否重寫載入該layout。預設返回NO,若返回YES,則重新執行這倆方法:
  • - (void)prepareLayout;
  • - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
  • - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity;返回layout“最終”的偏移量,何謂“最終”,手指離開螢幕時layout的偏移量不是最終的,因為它有慣性,當它停止時才是“最終”偏移量。

下面這兩個方法一般用於自定義插入刪除時的動畫,後面再說。

  • **- (UICollectionViewLayoutAttributes )initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath )itemIndexPath;

  • **- (nullable UICollectionViewLayoutAttributes )finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath )itemIndexPath;

本Demo的程式碼雖然子類化了UICollectionViewLayout,但是主要是用於自定義插入刪除動畫,所以本段沒什麼程式碼展示。


UICollectionView插入刪除的操作及動畫

插入刪除的操作

新增在哪觸發:

    UIBarButtonItem *btnItem = [[UIBarButtonItem alloc] initWithTitle:@"新增"
                                                                style:UIBarButtonItemStylePlain
                                                               target:self
                                                               action:@selector(addItemBtnClick:)];
    
    self.navigationItem.rightBarButtonItem = btnItem;

新增的實現:

// 新增(插入item)
- (void)addItemBtnClick:(UIBarButtonItem *)btnItem
{
    [_collectionView performBatchUpdates:^{
        // 構造一個indexPath
        NSIndexPath *indePath = [NSIndexPath indexPathForItem:_section0Array.count inSection:0];
        [_collectionView insertItemsAtIndexPaths:@[indePath]]; // 然後在此indexPath處插入給collectionView插入一個item
        [_section0Array addObject:@"x"]; // 保持collectionView的item和資料來源一致
    } completion:nil];
}

因為是練習Demo,所以暫時把刪除的觸發源寫在了長按某Item彈出選單的copy按鈕裡。實際中你可以自定義UICollectionViewCell,新增長按手勢,長按抖動出現叉號,然後刪除等,隨你怎麼做。

// copy and paste 的實現
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender
{
   if([NSStringFromSelector(action) isEqualToString:@"copy:"])
   {
//      NSLog(@"-------------執行拷貝-------------");
       [_collectionView performBatchUpdates:^{
           [_section0Array removeObjectAtIndex:indexPath.row];
           [_collectionView deleteItemsAtIndexPaths:@[indexPath]];
       } completion:nil];
   }
   else if([NSStringFromSelector(action) isEqualToString:@"paste:"])
   {
       NSLog(@"-------------執行貼上-------------");
   }
}

插入刪除的動畫

上面已經提到了在UICollectionViewLayout類中有兩個用於自定義動畫的方法,兩個方法分別表示動畫的起始狀態和終止狀態,我們可以分別在方法裡設定layoutAttrure來實現某種動畫效果。

蘋果選擇了一種安全的途徑去實現一個簡單的淡入淡出動畫作為所有佈局的預設動畫。如果你想實現自定義動畫,最好的辦法是子類化 UICollectionViewFlowLayout 並且在適當的地方實現你的動畫。

一般來說,我們對佈局屬性從初始狀態到結束狀態進行線性插值來計算 collection view 的動畫引數。然而,新插入或者刪除的元素並沒有最初或最終狀態來進行插值。要計算這樣的 cells 的動畫,collection view 將通過 initialLayoutAttributesForAppearingItemAtIndexPath: 以及 finalLayoutAttributesForDisappearingItemAtIndexPath: 方法來詢問其佈局物件,以獲取最初的和最後的屬性。蘋果預設的實現中,對於特定的某個 indexPath,返回的是它的通常的位置,但 alpha 值為 0.0,這就產生了一個淡入或淡出動畫。

簡而言之,就是蘋果自帶了插入刪除時Item的淡入淡出的動畫,若你想自定義更炫的動畫,就子類化UICollectionViewFlowLayout類,並重寫以下兩個方法:

// 初始狀態
- (nullable UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
    attr.center = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMaxY(self.collectionView.bounds));
    attr.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(0.2, 0.2), M_PI);

    return attr;
}


// 終結狀態
- (nullable UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        attr.alpha = 0.0f;
    
    return attr;
}

988593-ffa2c139ef3e4518.gif
insert&delete.gif

UICollectionView的轉場動畫

http://objccn.io/issue-12-5/

相關文章