UICollectionView詳解

UICollectionView入門介紹

什麼是UICollectionView

  • UICollectionView是一種新的數據展示方式,簡單來說可以把他理解成多列的UITableView(請一定注意這是UICollectionView的最最簡單的形式)。

  • 標準的UICollectionView包含三個部分,它們都是UIView的子類:

    • Cells 用於展示內容的主體,對於不同的cell可以指定不同尺寸和不同的內容,這個稍後再說
    • Supplementary Views 追加視圖 如果你對UITableView比較熟悉的話,可以理解爲每個Section的Header或者Footer,用來標記每個section的view
    • Decoration Views 裝飾視圖 這是每個section的背景,比如iBooks中的書架

這裏寫圖片描述

實現一個簡單的UICollectionView

  • 實現一個UICollectionView和實現一個UITableView基本沒有什麼大區別,它們都同樣是datasource和delegate設計模式的:datasource爲view提供數據源,告訴view要顯示些什麼東西以及如何顯示它們,delegate提供一些樣式的小細節以及用戶交互的相應。

  • UICollectionViewDataSource

    • section的數量 -numberOfSectionsInCollection:
    • 某個section裏有多少個item -collectionView:numberOfItemsInSection:
    • 對於某個位置應該顯示什麼樣的cell -collectionView:cellForItemAtIndexPath:

實現以上三個委託方法,基本上就可以保證CollectionView工作正常了。

  • 提供Supplementary View的方法
collectionView:viewForSupplementaryElementOfKind:atIndexPath:

關於重用

  • 在UICollectionView中,不僅cell可以重用,Supplementary View和Decoration View也是可以並且應當被重用的。
    • 在iOS5中,Apple對UITableView的重用做了簡化,以往要寫類似這樣的代碼:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MY_CELL_ID"];  
if (!cell) {    //如果沒有可重用的cell,那麼生成一個  
    cell = [[UITableViewCell alloc] init]; 
} 
//配置cell,blablabla 
return cell 
如果我們在TableView向數據源請求數據之前使用-registerNib:forCellReuseIdentifier:方法爲@“MYCELLID"註冊過nib的話,就可以省下每次判斷並初始化cell的代碼,要是在重用隊列裏沒有可用的cell的話,runtime將自動幫我們生成並初始化一個可用的cell。
  • 在UICollectionView中Apple繼承使用了這個特性,並且把其進行了一些擴展。使用以下方法進行註冊:
 -registerClass:forCellWithReuseIdentifier:
 -registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
 -registerNib:forCellWithReuseIdentifier:
 -registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
  • 相比UITableView有兩個主要變化:

    • 一是加入了對某個Class的註冊,這樣即使不用提供nib而是用代碼生成的view也可以被接受爲cell了;
    • 二是不僅只是cell,Supplementary View也可以用註冊的方法綁定初始化了。
  • 在對collection view的重用ID註冊後,就可以像UITableView那樣簡單的寫cell配置了:

- (UICollectionView*)collectionView:(UICollectionView*)cv cellForItemAtIndexPath:(NSIndexPath*)indexPath { 
    MyCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@”MY_CELL_ID”]; 
    // Configure the cell's content 
    cell.imageView.image = ... 
    return cell; 
}

需要吐槽的是,對collection view,取重用隊列的方法的名字和UITableView裏面不一樣了,在Identifier前面多加了Reuse五個字母,語義上要比以前清晰,命名規則也比以前嚴謹了

UICollectionViewDelegate

  • 由UICollectionViewDelegate負責:

    • cell的高亮
    • cell的選中狀態
    • 可以支持長按後的菜單
  • 關於用戶交互,UICollectionView也做了改進。

    • 每個cell現在有獨立的高亮事件和選中事件的delegate,用戶點擊cell的時候,現在會按照以下流程向delegate進行詢問:
1. -collectionView:shouldHighlightItemAtIndexPath: 是否應該高亮?
2. -collectionView:didHighlightItemAtIndexPath: 如果1回答爲是,那麼高亮
3. -collectionView:shouldSelectItemAtIndexPath: 無論1結果如何,都詢問是否可以被選中?
4. -collectionView:didUnhighlightItemAtIndexPath: 如果1回答爲是,那麼現在取消高亮
5. -collectionView:didSelectItemAtIndexPath: 如果3回答爲是,那麼選中cell

狀態控制要比以前靈活一些,對應的高亮和選中狀態分別由highlighted和selected兩個屬性表示。

關於Cell

  • UICollectionViewCell不存在各式各樣的默認的style,這主要是由於展示對象的性質決定的

    • 因此SDK提供給我們的默認的UICollectionViewCell結構上相對比較簡單,由下至上:
      • 1.首先是cell本身作爲容器view
      • 2.然後是一個大小自動適應整個cell的backgroundView,用作cell平時的背景
      • 3.再其上是selectedBackgroundView,是cell被選中時的背景
      • 4.最後是一個contentView,自定義內容應被加在這個view上

    這次Apple給我們帶來的好處是被選中cell的自動變化,所有的cell中的子view,也包括contentView中的子view,在當cell被選中時,會自動去查找view是否有被選中狀態下的改變。比如在contentView里加了一個normal和selected指定了不同圖片的imageView,那麼選中這個cell的同時這張圖片也會從normal變成selected,而不需要額外的任何代碼。

UICollectionViewLayout

  • UICollectionViewLayout可以說是UICollectionView的大腦和中樞,它負責了將各個cell、Supplementary View和Decoration Views進行組織,爲它們設定各自的屬性,包括但不限於:
    • 位置
    • 尺寸
    • 透明度
    • 層級關係
    • 形狀
    • 等等等等…
  • Layout決定了UICollectionView是如何顯示在界面上的。在展示之前,一般需要生成合適的UICollectionViewLayout子類對象,並將其賦予CollectionView的collectionViewLayout屬性。

Apple爲我們提供了一個最簡單可能也是最常用的默認layout對象,UICollectionViewFlowLayout。Flow Layout簡單說是一個直線對齊的layout,最常見的Grid View形式即爲一種Flow Layout配置。照片架界面就是一個典型的Flow Layout。

這裏寫圖片描述

  • 首先一個重要的屬性是itemSize,它定義了每一個item的大小。通過設定itemSize可以全局地改變所有cell的尺寸,如果想要對某個cell制定尺寸,可以使用-collectionView:layout:sizeForItemAtIndexPath:方法。
  • 間隔可以指定item之間的間隔和每一行之間的間隔,和size類似,有全局屬性,也可以對每一個item和每一個section做出設定:
@property (CGSize) minimumInteritemSpacing
@property (CGSize) minimumLineSpacing
- collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
- collectionView:layout:minimumLineSpacingForSectionAtIndex:
  • 滾動方向由屬性scrollDirection確定scrollview的方向,將影響Flow Layout的基本方向和由header及footer確定的section之間的寬度
UICollectionViewScrollDirectionVertical
UICollectionViewScrollDirectionHorizontal
  • Header和Footer尺寸同樣地分爲全局和部分。需要注意根據滾動方向不同,header和footer的高和寬中只有一個會起作用。垂直滾動時section間寬度爲該尺寸的高,而水平滾動時爲寬度起作用。
@property (CGSize) headerReferenceSize
@property (CGSize) footerReferenceSize
- collectionView:layout:referenceSizeForHeaderInSection:
- collectionView:layout:referenceSizeForFooterInSection:
  • 縮進
@property UIEdgeInsets sectionInset;
- collectionView:layout:insetForSectionAtIndex:

總結:
一個UICollectionView的實現包括兩個必要部分:
UICollectionViewDataSource和UICollectionViewLayout,和一個交互部分:UICollectionViewDelegate。

幾個自定義的Layout

  • UICollectionView的強大之處,就在於各種layout的自定義實現,以及它們之間的切換。先看幾個相當exiciting的例子吧~

    • 比如,堆疊佈局
      這裏寫圖片描述

    • 圓形佈局:
      這裏寫圖片描述

    • Cover Flow佈局:
      這裏寫圖片描述

  • 所有這些佈局都採用了同樣的數據源和委託方法,因此完全實現了model和view的解耦。


UICollectionView進階

UICollectionViewLayoutAttributes

  • UICollectionViewLayoutAttributes是一個非常重要的類,先來看看property列表:
@property (nonatomic) CGRect frame
@property (nonatomic) CGPoint center
@property (nonatomic) CGSize size
@property (nonatomic) CATransform3D transform3D
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
@property (nonatomic, getter=isHidden) BOOL hidden

可以看到,UICollectionViewLayoutAttributes的實例中包含了諸如邊框,中心點,大小,形狀,透明度,層次關係和是否隱藏等信息。當UICollectionView在獲取佈局時將針對每一個indexPath的部件(包括cell,追加視圖和裝飾視圖),向其上的UICollectionViewLayout實例詢問該部件的佈局信息(在這個層面上說的話,實現一個UICollectionViewLayout的時候,其實很像是zap一個delegate,之後的例子中會很明顯地看出),這個佈局信息,就以UICollectionViewLayoutAttributes的實例的方式給出。

自定義的UICollectionViewLayout

  • UICollectionViewLayout的功能爲向UICollectionView提供佈局信息,不僅包括cell的佈局信息,也包括追加視圖和裝飾視圖的佈局信息。
    • 實現一個自定義layout的常規做法是繼承UICollectionViewLayout類,然後重載下列方法:
-(CGSize)collectionViewContentSize   // 返回collectionView的內容的尺寸
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

// 返回rect中的所有的元素的佈局屬性
// 返回的是包含UICollectionViewLayoutAttributes的NSArray
// UICollectionViewLayoutAttributes可以是cell,追加視圖或裝飾視圖的信息,通過不同的UICollectionViewLayoutAttributes初始化方法可以得到不同類型的UICollectionViewLayoutAttributes:

 ① layoutAttributesForCellWithIndexPath:
 ② layoutAttributesForSupplementaryViewOfKind:withIndexPath:
 ③ layoutAttributesForDecorationViewOfKind:withIndexPath:
- (UICollectionViewLayoutAttributes_)layoutAttributesForItemAtIndexPath:(NSIndexPath _)indexPath
// 返回對應於indexPath的位置的cell的佈局屬性
- (UICollectionViewLayoutAttributes_)layoutAttributesForSupplementaryViewOfKind:(NSString _)kind atIndexPath:(NSIndexPath *)indexPath

// 返回對應於indexPath的位置的追加視圖的佈局屬性,如果沒有追加視圖可不重載
- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind: (NSString_)decorationViewKind atIndexPath:(NSIndexPath _)indexPath

// 返回對應於indexPath的位置的裝飾視圖的佈局屬性,如果沒有裝飾視圖可不重載
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds

// 當邊界發生改變時,是否應該刷新佈局。如果YES則在邊界變化(一般是scroll到其他地方)時,將重新計算需要的佈局信息。
  • 在初始化一個UICollectionViewLayout實例後,會有一系列準備方法被自動調用,以保證layout實例的正確。
    • 首先,-(void)prepareLayout將被調用,默認下該方法什麼沒做,但是在自己的子類實現中,一般在該方法中設定一些必要的layout的結構和初始需要的參數等。
    • 之後,-(CGSize) collectionViewContentSize將被調用,以確定collection應該佔據的尺寸。注意這裏的尺寸不是指可視部分的尺寸,而應該是所有內容所佔的尺寸。collectionView的本質是一個scrollView,因此需要這個尺寸來配置滾動行爲。
    • 接下來-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被調用。初始的layout的外觀將由該方法返回的UICollectionViewLayoutAttributes來決定。
    • 另外,在需要更新layout時,給當前layout發送 -invalidateLayout,該消息會立即返回,並且預約在下一個loop的時候刷新當前layout,這一點和UIView的setNeedsLayout方法十分類似。在-invalidateLayout後的下一個collectionView的刷新loop中,又會從prepareLayout開始,依次再調用-collectionViewContentSize和-layoutAttributesForElementsInRect來生成更新後的佈局。

案例 demo

LineLayout——對於個別UICollectionViewLayoutAttributes的調整

  • 先看LineLayout,它繼承了UICollectionViewFlowLayout這個Apple提供的基本的佈局。它主要實現了單行佈局,自動對齊到網格以及當前網格cell放大三個特性。如圖:

    這裏寫圖片描述

  • 先看LineLayout的init方法:

-(id)init
{
    self = [super init];
    if (self) {
        self.itemSize = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
        self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        self.sectionInset = UIEdgeInsetsMake(200, 0.0, 200, 0.0);
        self.minimumLineSpacing = 50.0;
    }
    return self;
}
self.sectionInset = UIEdgeInsetsMake(200, 0.0, 200, 0.0); 確定了縮進,此處爲上方和下方各縮進200個point。由於cell的size已經定義了爲200x200,因此屏幕上在縮進後就只有一排item的空間了。

self.minimumLineSpacing = 50.0; 這個定義了每個item在水平方向上的最小間距。

LineLayout通過重載父類方法後,可以實現一些新特性,比如這裏的動對齊到網格以及當前網格cell放大。
  • 自動對齊到網格
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    //proposedContentOffset是沒有對齊到網格時本來應該停下的位置 
    //計算出實際中心位置
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);

    //取當前屏幕中的UICollectionViewLayoutAttributes
    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    NSArray* array = [super layoutAttributesForElementsInRect:targetRect];

    //對當前屏幕中的UICollectionViewLayoutAttributes逐個與屏幕中心進行比較,找出最接近中心的一個
    for (UICollectionViewLayoutAttributes* layoutAttributes in array) {
        CGFloat itemHorizontalCenter = layoutAttributes.center.x;
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }

    //返回調整好的point
    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
  • 當前item放大
//佈局屬性
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    //取父類的UICollectionViewLayoutAttributes
    NSArray* array = [super layoutAttributesForElementsInRect:rect];

    //可視rect
    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;

    //設置item的縮放
    for (UICollectionViewLayoutAttributes* attributes in array) {
        if (CGRectIntersectsRect(attributes.frame, rect)) {
            CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;//item到中心點的距離
            CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;//距離除以有效距離得到標準化距離
            //距離小於有效距離才生效
            if (ABS(distance) < ACTIVE_DISTANCE) {
                CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));//縮放率範圍1~1.3,與標準距離負相關
                attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);//x,y軸方向變換
                attributes.zIndex = 1;
            }
        }
    }   
    return array;
}

對於個別UICollectionViewLayoutAttributes進行調整,以達到滿足設計需求是UICollectionView使用中的一種思路。在根據位置提供不同layout屬性的時候,需要記得讓-shouldInvalidateLayoutForBoundsChange:返回YES,這樣當邊界改變的時候,-invalidateLayout會自動被髮送,才能讓layout得到刷新。

CircleLayout——完全自定義的Layout,添加刪除item,以及手勢識別

  • CircleLayout的例子稍微複雜一些,cell分佈在圓周上,點擊cell的話會將其從collectionView中移出,點擊空白處會加入一個cell,加入和移出都有動畫效果。

    這裏寫圖片描述

  • 首先,佈局準備中定義了一些之後計算所需要用到的參數。

-(void)prepareLayout
{
    //和init相似,必須call super的prepareLayout以保證初始化正確
    [super prepareLayout];

    //其實對於一個size不變的collectionView來說,除了_cellCount之外的中心和半徑的定義也可以扔到init裏去做,但是顯然在prepareLayout裏做的話具有更大的靈活性。因爲每次重新給出layout時都會調用prepareLayout,這樣在以後如果有collectionView大小變化的需求時也可以自動適應變化
    CGSize size = self.collectionView.frame.size;
    _cellCount = [[self collectionView] numberOfItemsInSection:0];
    _center = CGPointMake(size.width / 2.0, size.height / 2.0);
    _radius = MIN(size.width, size.height) / 2.5;
}
  • 然後,按照UICollectionViewLayout子類的要求,重載了所需要的方法:
//整個collectionView的內容大小就是collectionView的大小(沒有滾動)
-(CGSize)collectionViewContentSize
{
    return [self collectionView].frame.size;
}

//通過所在的indexPath確定位置。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path]; //生成空白的attributes對象,其中只記錄了類型是cell以及對應的位置是indexPath
    //配置attributes到圓周上
    attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
    attributes.center = CGPointMake(_center.x + _radius * cosf(2 * path.item * M_PI / _cellCount), _center.y + _radius * sinf(2 * path.item * M_PI / _cellCount));
    return attributes;
}

//用來在一開始給出一套UICollectionViewLayoutAttributes
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray* attributes = [NSMutableArray array];
    for (NSInteger i=0 ; i &lt; self.cellCount; i++) {
        //這裏利用了-layoutAttributesForItemAtIndexPath:來獲取attributes
        NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
    }    
    return attributes;
}
  • 現在已經得到了一個circle layout。爲了實現cell的添加和刪除,需要爲collectionView加上手勢識別,這個很簡單,在ViewController中:
UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];  
[self.collectionView addGestureRecognizer:tapRecognizer];
  • 對應的處理方法handleTapGesture:
- (void)handleTapGesture:(UITapGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateEnded) {
        CGPoint initialPinchPoint = [sender locationInView:self.collectionView];
        NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:initialPinchPoint]; //獲取點擊處的cell的indexPath
        if (tappedCellPath!=nil) { //點擊處沒有cell
            self.cellCount = self.cellCount - 1;
            [self.collectionView performBatchUpdates:^{
                [self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:tappedCellPath]];
            } completion:nil];
        } else {
            self.cellCount = self.cellCount + 1;
            [self.collectionView performBatchUpdates:^{
                [self.collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]];
            } completion:nil];
        }
    }
}
  • performBatchUpdates:completion: 再次展示了block的強大的一面..這個方法可以用來對collectionView中的元素進行批量的插入,刪除,移動等操作,同時將觸發collectionView所對應的layout的對應的動畫。相應的動畫由layout中的下列四個方法來定義:
initialLayoutAttributesForAppearingItemAtIndexPath:

initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:

finalLayoutAttributesForDisappearingItemAtIndexPath:

finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
// 更正:正式版中API發生了變化 initialLayoutAttributesForInsertedItemAtIndexPath:在正式版中已經被廢除。現在在insert或者delete之前,prepareForCollectionViewUpdates:會被調用,可以使用這個方法來完成添加/刪除的佈局。
  • 在CircleLayout中,實現了cell的動畫。
//插入前,cell在圓心位置,全透明
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForInsertedItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
    attributes.alpha = 0.0;
    attributes.center = CGPointMake(_center.x, _center.y);
    return attributes;
}

//刪除時,cell在圓心位置,全透明,且只有原來的1/10大
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDeletedItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
    attributes.alpha = 0.0;
    attributes.center = CGPointMake(_center.x, _center.y);
    attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
    return attributes;
}

佈局之間的切換

  • 有時候可能需要不同的佈局,Apple也提供了方便的佈局間切換的方法。直接更改collectionView的collectionViewLayout屬性可以立即切換佈局。而如果通過setCollectionViewLayout:animated:,則可以在切換佈局的同時,使用動畫來過渡。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章