UICollectionView的使用


UITableView中我們使用datasource和delegate分別處理我們的數據和交互,而且UITableView默認提供了兩種樣式供我們選擇如何呈現數據,在IOS6中蘋果提供了UICollectionView用來更自由地定制呈現我們的數據。

UICollectionView使用包括三個部分:

1.設置數據(使用UICollectionViewDataSource)

2.設置數據呈現方式(使用UICollectionViewLayout)

3.設置界面交互(使用UICollectionViewDelegate)

其中1,3和UITableView一致,可見UICollectionView比UITableView更具有一般性(我們可以使用UICollectionView實現UITableView的效果)

本篇博客的outline如下(本文參考http://www.onevcat.com/2012/06/introducing-collection-views/,代碼下載地址為https://github.com/zanglitao/UICollectionViewDemo

1:基本介紹

2:UICollectionViewDataSource和UICollectionViewDelegate介紹

3:使用UICollectionViewFlowLayout

4:UICollectionViewFlowLayout的擴展

5:使用自定義UICollectionViewLayout

6:添加和刪除數據

7:布局切換

 

基本介紹

UICollectionView是一種新的數據展示方式,簡單來說可以把他理解成多列的UITableView(請一定注意這是UICollectionView的最最簡單的形式)。如果你用過iBooks的話,可能你還對書架布局有一定印象:一個虛擬書架上放着你下載和購買的各類圖書,整齊排列。其實這就是一個UICollectionView的表現形式,或者iPad的iOS6中的原生時鍾應用中的各個時鍾,也是UICollectionView的最簡單的一個布局,如圖:

iOS6 iPad版時鍾應用 最簡單的UICollectionView就是一個GridView,可以以多列的方式將數據進行展示。標准的UICollectionView包含三個部分,它們都是UIView的子類:

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

 

不管一個UICollectionView的布局如何變化,這三個部件都是存在的。再次說明,復雜的UICollectionView絕不止上面的幾幅圖。

 

UICollectionViewDataSource和UICollectionViewDelegate介紹

UICollectionViewDataSource用來設置數據,此協議包含的方法如下

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section; //設置每個section包含的item數目

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; //返回對應indexPath的cell

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView; //返回section的數目,此方法可選,默認返回1

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; //返回Supplementary Views,此方法可選

 

對於Decoration Views,提供方法並不在UICollectionViewDataSource中,而是直接UICollectionViewLayout類中的(因為它僅僅是視圖相關,而與數據無關),放到稍后再說。

與UITableViewCell相似的是UICollectionViewCell也支持重用,典型的UITbleViewCell重用寫法如下

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MY_CELL_ID"];  
if (!cell) {    //如果沒有可重用的cell,那么生成一個  
    cell = [[UITableViewCell alloc] init]; 
} 
//配置cell,blablabla 
return cell 

 

UICollectionViewCell重用寫法於UITableViewCell一致,但是現在更簡便的是如果我們直接在storyboard中對cell設置了identifier,或者使用了以下方法進行注冊

  • -registerClass:forCellWithReuseIdentifier:
  • -registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
  • -registerNib:forCellWithReuseIdentifier:
  • -registerNib:forSupplementaryViewOfKind:withReuseIdentifier:

那么可以更簡單地實現重用

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

上面的4個語句分別提供了nib和class方法對collectionViewCell和supplementaryView進行注冊

 

UICollectionViewDelegate處理交互,包括cell點擊事件,cell點擊后高亮效果以及長按菜單等設置,當用戶點擊cell后,會依次執行協議中以下方法

  1. -collectionView:shouldHighlightItemAtIndexPath: 是否應該高亮?
  2. -collectionView:didHighlightItemAtIndexPath: 如果1回答為是,那么高亮
  3. -collectionView:shouldSelectItemAtIndexPath: 無論1結果如何,都詢問是否可以被選中?
  4. -collectionView:didUnhighlightItemAtIndexPath: 如果1回答為是,那么現在取消高亮
  5. -collectionView:didSelectItemAtIndexPath: 如果3回答為是,那么選中cell

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

關於Cell

相對於UITableViewCell來說,UICollectionViewCell沒有這么多花頭。首先UICollectionViewCell不存在各式各樣的默認的style,這主要是由於展示對象的性質決定的,因為UICollectionView所用來展示的對象相比UITableView來說要來得靈活,大部分情況下更偏向於圖像而非文字,因此需求將會千奇百怪。因此SDK提供給我們的默認的UICollectionViewCell結構上相對比較簡單,由下至上:

  • 首先是cell本身作為容器view
  • 然后是一個大小自動適應整個cell的backgroundView,用作cell平時的背景
  • 再其上是selectedBackgroundView,是cell被選中時的背景
  • 最后是一個contentView,自定義內容應被加在這個view上

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

 

使用UICollectionViewFlowLayout

UICollectionViewLayout用來處理數據的布局,通過它我們可以設置每個cell,Supplementary View以及Decoration Views的呈現方式,比如位置,大小,透明度,形狀等等屬性

Layout決定了UICollectionView是如何顯示在界面上的。在展示之前,一般需要生成合適的UICollectionViewLayout子類對象,並將其賦予CollectionView的collectionViewLayout屬性,蘋果還提供了一個現成的UICollectionViewFlowLayout,通過這個layout我們可以很簡單地實現流布局,UICollectionViewFlowLayout常用的配置屬性如下

  • CGSize itemSize:它定義了每一個item的大小。通過設定itemSize可以全局地改變所有cell的尺寸,如果想要對某個cell制定尺寸,可以使用-collectionView:layout:sizeForItemAtIndexPath:方法。
  • CGFloat minimumLineSpacing:每一行的間距
  • CGFloat minimumInteritemSpacing:item與item的間距
  • UIEdgeInsets sectionInset:每個section的縮進
  • UICollectionViewScrollDirection scrollDirection:設定是垂直流布局還是橫向流布局,默認是UICollectionViewScrollDirectionVertical
  • CGSize headerReferenceSize:設定header尺寸
  • CGSize footerReferenceSize:設定footer尺寸

上面都是全局屬性的設置,我們可以通過delegate中的方法對進行定制,通過實現以下這些方法設定的屬性的優先級比全局設定的要高

@protocol UICollectionViewDelegateFlowLayout <UICollectionViewDelegate>
@optional

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;

@end

 

接下來我們使用使用UICollectionViewFlowLayout完成一個簡單demo

1:設置我們的cell

//SimpleFlowLayoutCell.h
@interface SimpleFlowLayoutCell : UICollectionViewCell
@property(nonatomic,strong)UILabel *label;
@end

//SimpleFlowLayoutCell.m
@implementation SimpleFlowLayoutCell

-(id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.height)];
        self.label.textAlignment = NSTextAlignmentCenter;
        self.label.textColor = [UIColor blackColor];
        self.label.font = [UIFont boldSystemFontOfSize:15.0];
        self.backgroundColor = [UIColor lightGrayColor];
        
        [self.contentView addSubview:self.label];
        
        self.contentView.layer.borderWidth = 1.0f;
        self.contentView.layer.borderColor = [UIColor blackColor].CGColor;
    }
    
    return self;
}

@end

2:設置追加視圖

//SimpleFlowLayoutSupplementaryView.h
@interface SimpleFlowLayoutSupplementaryView : UICollectionReusableView
@property(nonatomic,strong)UILabel *label;
@end

//SimpleFlowLayoutSupplementaryView.m
@implementation SimpleFlowLayoutSupplementaryView

-(id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.height)];
        self.label.textAlignment = NSTextAlignmentCenter;
        self.label.textColor = [UIColor blackColor];
        self.label.font = [UIFont boldSystemFontOfSize:15.0];
        self.backgroundColor = [UIColor lightGrayColor];
        
        [self addSubview:self.label];
        
        self.layer.borderWidth = 1.0f;
        self.layer.borderColor = [UIColor blackColor].CGColor;
    }
    
    return self;
}

@end

 

3:使用流布局初始化我們的UICollectionView

- (void)viewDidLoad {
    [super viewDidLoad];

    self.collectionView = [[UICollectionView alloc] initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
    self.collectionView.backgroundColor = [UIColor whiteColor];
    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;
    
    [self.collectionView registerClass:[SimpleFlowLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
    //追加視圖的類型是UICollectionElementKindSectionHeader,也可以設置為UICollectionElementKindSectionFooter
    [self.collectionView registerClass:[SimpleFlowLayoutSupplementaryView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"MY_SUPPLEMENT"];
    
    [self.view addSubview:self.collectionView];
}

 

4:配置datasource

//每個section中有32個item
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return  32;
}


- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    SimpleFlowLayoutCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellidentifier forIndexPath:indexPath];
    cell.label.text = [NSString stringWithFormat:@"%d",indexPath.item];
    return cell;
}

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

// 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 {
    SimpleFlowLayoutSupplementaryView *view = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"MY_SUPPLEMENT" forIndexPath:indexPath];
    view.label.text = [NSString stringWithFormat:@"section header %d",indexPath.section];
    return view;
}

此時運行程序可以看到如下界面

 程序並沒有顯示我們設置的header視圖,這是因為我們使用的是UICollectionViewFlowLayout默認配置,當前header視圖高度為0,我們可以通過設置UICollectionViewFlowLayout的

headerReferenceSize屬性改變大小,也可以通過協議方法返回特定section的header大小,這里我們先使用后者

我們添加以下方法

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

此時再運行就能得到以下結果

 

5:配置layout

上面的代碼使用了flowlayout默認的配置,包括itemsize,行間距,item間距,追加視圖大小等等都是默認值,我們可以改變這些值

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    self.collectionView = [[UICollectionView alloc] initWithFrame:[UIScreen mainScreen].bounds collectionViewLayout:layout];
    self.collectionView.backgroundColor = [UIColor whiteColor];
    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;
    
    [self.collectionView registerClass:[SimpleFlowLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
    //追加視圖的類型是UICollectionElementKindSectionHeader,也可以設置為UICollectionElementKindSectionFooter
    [self.collectionView registerClass:[SimpleFlowLayoutSupplementaryView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"MY_SUPPLEMENT"];
    
    [self.view addSubview:self.collectionView];
    
    //配置UICollectionViewFlowLayout屬性
    //每個itemsize的大小
    layout.itemSize = CGSizeMake(80, 50);
    //行與行的最小間距
    layout.minimumLineSpacing = 44;
    
    //每行的item與item之間最小間隔(如果)
    layout.minimumInteritemSpacing = 20;
    //每個section的頭部大小
    layout.headerReferenceSize = CGSizeMake(44, 44);
    //每個section距離上方和下方20,左方和右方10
    layout.sectionInset = UIEdgeInsetsMake(20, 10, 20, 10);
    //垂直滾動(水平滾動設置UICollectionViewScrollDirectionHorizontal)
    layout.scrollDirection = UICollectionViewScrollDirectionVertical;
}

運行結果如下

 

6:修改特定cell大小

包括上面配置header高度時使用的方法- collectionView:layout:referenceSizeForHeaderInSection:

UICollectionViewDelegateFlowLayout還提供了方法對特定cell大小,間距進行設置

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0) {
        return CGSizeMake(80, 40);
    } else {
        return CGSizeMake(40, 40);
    }
}

7:設置delegate,通過delegate中的方法可以設置cell的點擊事件,這部分和UITableView差不多

 

UICollectionViewFlowLayout的擴展

上一部分我們直接使用了UICollectionViewFlowLayout,我們也可以繼承此布局實現更多的效果,蘋果官方給出了一個flowlayout的demo,實現滾動時item放大以及網格對齊的功能

 

1:新建我們的cell類

//LineLayoutCell.h
@interface LineLayoutCell : UICollectionViewCell
@property (strong, nonatomic) UILabel* label;
@end

//LineLayoutCell.m
@implementation LineLayoutCell

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.height)];
        self.label.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
        self.label.textAlignment = NSTextAlignmentCenter;
        self.label.font = [UIFont boldSystemFontOfSize:50.0];
        self.label.backgroundColor = [UIColor underPageBackgroundColor];
        self.label.textColor = [UIColor blackColor];
        [self.contentView addSubview:self.label];;
        self.contentView.layer.borderWidth = 1.0f;
        self.contentView.layer.borderColor = [UIColor whiteColor].CGColor;
    }
    return self;
}

@end

 

2:storyboard中新建UICollectionViewController,設置類為我們自定義的LineCollectionViewController,並設置Layout為我們自定義的LineLayout

 

3:在我們自定義的LineCollectionViewController中配置數據源

//LineCollectionViewController.h
@interface LineCollectionViewController : UICollectionViewController
@end

//LineCollectionViewController.m
@implementation LineCollectionViewController

-(void)viewDidLoad
{
    [self.collectionView registerClass:[LineLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
}

- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section;
{
    return 60;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
    LineLayoutCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"MY_CELL" forIndexPath:indexPath];
    cell.label.text = [NSString stringWithFormat:@"%d",indexPath.item];
    return cell;
}
@end

 

4:設置LineLayout 

我們設置數據橫向滾動,item大小為CGSizeMake(200, 200),並設置每列數據上下各間隔200,這樣一行只有一列數據

//由於使用了storyboard的關系,需要使用initWithCoder
-(id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    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;
}

 

然后設置item滾動居中,只需要實現方法-targetContentOffsetForProposedContentOffset:withScrollingVelocity,此方法第一個參數為不加偏移量預期滾動停止時的ContentOffset,返回值類型為CGPoint,代表x,y的偏移

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat offsetAdjustment = MAXFLOAT;
    
    //預期滾動停止時水平方向的中心點
    CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);
    
    //預期滾動停止時顯示在屏幕上的區域
    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    
    //獲取該區域的UICollectionViewLayoutAttributes集合
    NSArray* array = [super layoutAttributesForElementsInRect:targetRect];
    
    
    for (UICollectionViewLayoutAttributes* layoutAttributes in array) {
        CGFloat itemHorizontalCenter = layoutAttributes.center.x;
        //循環結束后offsetAdjustment的值就是預期滾定停止后離水平方向中心點最近的item的中心店
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }
    
    //返回偏移量
    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

 上面的代碼中出現了一個新的類 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的實例中包含了諸如邊框,中心點,大小,形狀,透明度,層次關系和是否隱藏等信息。和DataSource的行為十分類似,當UICollectionView在獲取布局時將針對每一個indexPath的部件(包括cell,追加視圖和裝飾視圖),向其上的UICollectionViewLayout實例詢問該部件的布局信息(在這個層面上說的話,實現一個UICollectionViewLayout的時候,其實很像是zap一個delegate,之后的例子中會很明顯地看出),這個布局信息,就以UICollectionViewLayoutAttributes的實例的方式給出。

 

接下來設置item滾動過程中放大縮小效果

#define ACTIVE_DISTANCE 200
#define ZOOM_FACTOR 0.3
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    //獲取rect區域的UICollectionViewLayoutAttributes集合
    NSArray* array = [super layoutAttributesForElementsInRect:rect];
    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;
    
    for (UICollectionViewLayoutAttributes* attributes in array) {
        //只處理可視區域內的item
        if (CGRectIntersectsRect(attributes.frame, rect)) {
            //可視區域中心點與item中心點距離
            CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
            
            CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;
            if (ABS(distance) < ACTIVE_DISTANCE) {
                //放大系數
                //當可視區域中心點和item中心點距離為0時達到最大放大倍數1.3
                //當可視區域中心點和item中心點距離大於200時達到最小放大倍數1,也就是不放大
                //距離在0~200之間時放大倍數在1.3~1
                CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));
                attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
                attributes.zIndex = 1;
            }
        }
    }
    return array;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)oldBounds
{
    return YES;
} 

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

 

5:運行程序查看結果

 

使用自定義UICollectionViewLayout

 如果我們想實現更加復雜的布局,那就必須自定義我們自己的UICollectionView,實現一個自定義layout的常規做法是繼承UICollectionViewLayout類,然后重載下列方法

  • -(CGSize)collectionViewContentSize:返回collectionView內容的尺寸,
  • -(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect:返回rect范圍內所有元素的屬性數組,屬性是UICollectionViewLayoutAttributes,通過這個屬性數組就能決定每個元素的布局樣式

UICollectionViewLayoutAttributes可以是cell,追加視圖或裝飾視圖的信息,通過以下三種不同的UICollectionViewLayoutAttributes初始化方法可以得到不同類型的UICollectionViewLayoutAttributes    

  1. layoutAttributesForCellWithIndexPath:
  2. layoutAttributesForSupplementaryViewOfKind:withIndexPath:
  3. layoutAttributesForDecorationViewOfKind:withIndexPath:

 

  • - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path:返回對應於indexPath的元素的屬性
  • -(UICollectionViewLayoutAttributes _)layoutAttributesForItemAtIndexPath:(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來生成更新后的布局。

 

蘋果官方給出了一個circlelayout的demo

 1:新建我們的cell類

//CircleLayoutCell.h
@interface CircleLayoutCell : UICollectionViewCell
@end

//CircleLayoutCell.m
@implementation CircleLayoutCell
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.contentView.layer.cornerRadius = 35.0;
        self.contentView.layer.borderWidth = 1.0f;
        self.contentView.layer.borderColor = [UIColor whiteColor].CGColor;
        self.contentView.backgroundColor = [UIColor underPageBackgroundColor];
    }
    return self;
}
@end

 

2:storyboard中新建UICollectionViewController,設置類為我們自定義的CircleCollectionViewController,並設置Layout為我們自定義的CircleLayout

 

3:在我們自定義的CircleCollectionViewController中配置數據源

//CircleCollectionViewController.h
@interface CircleCollectionViewController : UICollectionViewController
@end

//CircleCollectionViewController.m
@interface CircleCollectionViewController ()
@property (nonatomic, assign) NSInteger cellCount;
@end

@implementation CircleCollectionViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.cellCount = 20;
    [self.collectionView registerClass:[CircleLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
    self.collectionView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
}

- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section;
{
    return self.cellCount;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
    CircleLayoutCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"MY_CELL" forIndexPath:indexPath];
    return cell;
}

@end

  

4:設置CircleLayout 

首先在prepareLayout中設置界面圓心的位置以及半徑

-(void)prepareLayout
{
    [super prepareLayout];
    
    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;
}

其實對於一個size不變的collectionView來說,除了_cellCount之外的中心和半徑的定義也可以扔到init里去做,但是顯然在prepareLayout里做的話具有更大的靈活性。因為每次重新給出layout時都會調用prepareLayout,這樣在以后如果有collectionView大小變化的需求時也可以自動適應變化

 

之后設置內容collectionView內容的尺寸,這個demo中內容尺寸就是屏幕可視區域

-(CGSize)collectionViewContentSize
{
    return [self collectionView].frame.size;
}

 

接下來在-layoutAttributesForElementsInRect中返回各個元素屬性組成的屬性數組

-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray* attributes = [NSMutableArray array];
    for (NSInteger i=0 ; i < self.cellCount; i++) {
        NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
    }
    return attributes;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    //初始化一個UICollectionViewLayoutAttributes
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
    
    //元素的大小
    attributes.size = CGSizeMake(70, 70);
    
    //元素的中心點
    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;
}

 

5:運行程序查看結果

 

添加和刪除數據

我們經常需要在collectionview中動態地添加一個元素或者刪除一個元素,collectionview提供了下面的函數處理數據的刪除與添加

  • -deleteItemsAtIndexPaths:刪除對應indexPath處的元素
  • -insertItemsAtIndexPaths:在indexPath位置處添加一個元素
  • -performBatchUpdates:completion:這個方法可以用來對collectionView中的元素進行批量的插入,刪除,移動等操作

繼續上面的CircleLayout的demo,我們為collectionView添加點擊事件,如果點擊某個元素則刪除此元素,如果點擊元素外的區域則在第一個位置新加一個元素

//CircleCollectionViewController.m
@implementation CircleCollectionViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.cellCount = 20;
    [self.collectionView registerClass:[CircleLayoutCell class] forCellWithReuseIdentifier:@"MY_CELL"];
    self.collectionView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
    
    
    UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
    [self.collectionView addGestureRecognizer:tapRecognizer];
}

- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section;
{
    return self.cellCount;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
    CircleLayoutCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"MY_CELL" forIndexPath:indexPath];
    return cell;
}

- (void)handleTapGesture:(UITapGestureRecognizer *)sender {
    
    if (sender.state == UIGestureRecognizerStateEnded)
    {
        CGPoint initialPinchPoint = [sender locationInView:self.collectionView];
        NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:initialPinchPoint];
        if (tappedCellPath!=nil)
        {
            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];
        }
    }
}

@end

有時候我們希望給刪除和添加元素加點動畫,layout中提供了下列方法處理動畫

  • initialLayoutAttributesForAppearingItemAtIndexPath:
  • initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
  • finalLayoutAttributesForDisappearingItemAtIndexPath:
  • finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:

需要注意的是以上4個方法會對所有顯示的元素調用,所以我們需要兩個數組放置剛添加或者刪除的元素,只對它們進行動畫處理,在insert或者delete之前prepareForCollectionViewUpdates:會被調用,insert或者delete之后finalizeCollectionViewUpdates:會被調用,我們可以在這兩個方法中設置和銷毀我們的數組

CircleLayout的完整代碼如下

//CircleLayout.m
#define ITEM_SIZE 70

@interface CircleLayout()

// arrays to keep track of insert, delete index paths
@property (nonatomic, strong) NSMutableArray *deleteIndexPaths;
@property (nonatomic, strong) NSMutableArray *insertIndexPaths;

@end

@implementation CircleLayout


-(void)prepareLayout
{
    [super prepareLayout];
    
    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;
}

-(CGSize)collectionViewContentSize
{
    return [self collectionView].frame.size;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
    //初始化一個UICollectionViewLayoutAttributes
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
    
    //元素的大小
    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;
}

-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray* attributes = [NSMutableArray array];
    for (NSInteger i=0 ; i < self.cellCount; i++) {
        NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
    }
    return attributes;
}

- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    // Keep track of insert and delete index paths
    [super prepareForCollectionViewUpdates:updateItems];
    
    self.deleteIndexPaths = [NSMutableArray array];
    self.insertIndexPaths = [NSMutableArray array];
    
    for (UICollectionViewUpdateItem *update in updateItems)
    {
        if (update.updateAction == UICollectionUpdateActionDelete)
        {
            [self.deleteIndexPaths addObject:update.indexPathBeforeUpdate];
        }
        else if (update.updateAction == UICollectionUpdateActionInsert)
        {
            [self.insertIndexPaths addObject:update.indexPathAfterUpdate];
        }
    }
}

- (void)finalizeCollectionViewUpdates
{
    [super finalizeCollectionViewUpdates];
    // release the insert and delete index paths
    self.deleteIndexPaths = nil;
    self.insertIndexPaths = nil;
}

// Note: name of method changed
// Also this gets called for all visible cells (not just the inserted ones) and
// even gets called when deleting cells!
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    // Must call super
    UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
    
    if ([self.insertIndexPaths containsObject:itemIndexPath])
    {
        // only change attributes on inserted cells
        if (!attributes)
            attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        
        // Configure attributes ...
        attributes.alpha = 0.0;
        attributes.center = CGPointMake(_center.x, _center.y);
    }
    
    return attributes;
}

// Note: name of method changed
// Also this gets called for all visible cells (not just the deleted ones) and
// even gets called when inserting cells!
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    // So far, calling super hasn't been strictly necessary here, but leaving it in
    // for good measure
    UICollectionViewLayoutAttributes *attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath];
    
    if ([self.deleteIndexPaths containsObject:itemIndexPath])
    {
        // only change attributes on deleted cells
        if (!attributes)
            attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        
        // Configure attributes ...
        attributes.alpha = 0.0;
        attributes.center = CGPointMake(_center.x, _center.y);
        attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
    }
    
    return attributes;
}

@end

  

布局切換

UICollectionView最大的好處是數據源,交互與布局的獨立和解耦,我們可以方便地使用一套數據在幾種布局中切換,直接更改collectionView的collectionViewLayout屬性可以立即切換布局。而如果通過setCollectionViewLayout:animated:,則可以在切換布局的同時,使用動畫來過渡。對於每一個cell,都將有對應的UIView動畫進行對應

 


免責聲明!

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



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