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的最簡單的一個布局,如圖:
最簡單的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后,會依次執行協議中以下方法
- -collectionView:shouldHighlightItemAtIndexPath: 是否應該高亮?
- -collectionView:didHighlightItemAtIndexPath: 如果1回答為是,那么高亮
- -collectionView:shouldSelectItemAtIndexPath: 無論1結果如何,都詢問是否可以被選中?
- -collectionView:didUnhighlightItemAtIndexPath: 如果1回答為是,那么現在取消高亮
- -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
- layoutAttributesForCellWithIndexPath:
- layoutAttributesForSupplementaryViewOfKind:withIndexPath:
- 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動畫進行對應