//聯系人:石虎QQ: 1224614774昵稱:嗡嘛呢叭咪哄
這是博主的WWDC2012筆記系列中的一篇,完整的筆記列表可以參看這里。如果您是首次來到本站,也許您會有興趣通過RSS,或者通過頁面左側的郵件訂閱的方式訂閱本站。
在上一篇UICollectionView的入門介紹中,大概地對iOS6新加入的強大的UICollectionView進行了一些說明。在這篇博文中,將結合WWDC2012 Session219:Advanced Collection View的內容,對Collection View進行一個深入的使用探討,并給出一個自定義的Demo。
首先回顧一下Collection View的構成,我們能看到的有三個部分:
Cells
Supplementary Views 追加視圖 (類似Header或者Footer)
Decoration Views 裝飾視圖 (用作背景展示)
而在表面下,由兩個方面對UICollectionView進行支持。其中之一和tableView一樣,即提供數據的UICollectionViewDataSource以及處理用戶交互的UICollectionViewDelegate。另一方面,對于cell的樣式和組織方式,由于collectionView比tableView要復雜得多,因此沒有按照類似于tableView的style的方式來定義,而是專門使用了一個類來對collectionView的布局和行為進行描述,這就是UICollectionViewLayout。
這次的筆記將把重點放在UICollectionViewLayout上,因為這不僅是collectionView和tableView的最重要求的區別,也是整個UICollectionView的精髓所在。
如果對UICollectionView的基本構成要素和使用方法還不清楚的話,可以移步到我之前的一篇筆記:Session筆記——205 Introducing Collection Views中進行一些了解。
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的實例的方式給出。
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最能解決問題。Apple官方給了一個flow layout和一個circle layout的例子,都很經典,需要的同學可以從這里下載。
LineLayout——對于個別UICollectionViewLayoutAttributes的調整
先看LineLayout,它繼承了UICollectionViewFlowLayout這個Apple提供的基本的布局。它主要實現了單行布局,自動對齊到網格以及當前網格cell放大三個特性。
先看LineLayout的init方法:
1234567891011
-(id)init{self=[superinit];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;}returnself;}
self.sectionInset = UIEdgeInsetsMake(200, 0.0, 200, 0.0); 確定了縮進,此處為上方和下方各縮進200個point。由于cell的size已經定義了為200x200,因此屏幕上在縮進后就只有一排item的空間了。
self.minimumLineSpacing = 50.0; 這個定義了每個item在水平方向上的最小間距。
UICollectionViewFlowLayout是Apple為我們準備的開袋即食的現成布局,因此之前提到的幾個必須重載的方法中需要我們操心的很少,即使完全不重載它們,現在也可以得到一個不錯的線狀一行的gridview了。而我們的LineLayout通過重載父類方法后,可以實現一些新特性,比如這里的動對齊到網格以及當前網格cell放大。
自動對齊到網格
1234567891011121314151617
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffsetwithScrollingVelocity:(CGPoint)velocity{//proposedContentOffset是沒有對齊到網格時本來應該停下的位置CGFloatoffsetAdjustment=MAXFLOAT;CGFloathorizontalCenter=proposedContentOffset.x+(CGRectGetWidth(self.collectionView.bounds)/2.0);CGRecttargetRect=CGRectMake(proposedContentOffset.x,0.0,self.collectionView.bounds.size.width,self.collectionView.bounds.size.height);NSArray*array=[superlayoutAttributesForElementsInRect:targetRect];//對當前屏幕中的UICollectionViewLayoutAttributes逐個與屏幕中心進行比較,找出最接近中心的一個for(UICollectionViewLayoutAttributes*layoutAttributesinarray){CGFloatitemHorizontalCenter=layoutAttributes.center.x;if(ABS(itemHorizontalCenter-horizontalCenter)<ABS(offsetAdjustment)){offsetAdjustment=itemHorizontalCenter-horizontalCenter;}}returnCGPointMake(proposedContentOffset.x+offsetAdjustment,proposedContentOffset.y);}
當前item放大
1234567891011121314151617181920
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{NSArray*array=[superlayoutAttributesForElementsInRect:rect];CGRectvisibleRect;visibleRect.origin=self.collectionView.contentOffset;visibleRect.size=self.collectionView.bounds.size;for(UICollectionViewLayoutAttributes*attributesinarray){if(CGRectIntersectsRect(attributes.frame,rect)){CGFloatdistance=CGRectGetMidX(visibleRect)-attributes.center.x;CGFloatnormalizedDistance=distance/ACTIVE_DISTANCE;if(ABS(distance)<ACTIVE_DISTANCE){CGFloatzoom=1+ZOOM_FACTOR*(1-ABS(normalizedDistance));attributes.transform3D=CATransform3DMakeScale(zoom,zoom,1.0);attributes.zIndex=1;}}}returnarray;}
對于個別UICollectionViewLayoutAttributes進行調整,以達到滿足設計需求是UICollectionView使用中的一種思路。在根據位置提供不同layout屬性的時候,需要記得讓-shouldInvalidateLayoutForBoundsChange:返回YES,這樣當邊界改變的時候,-invalidateLayout會自動被發送,才能讓layout得到刷新。
CircleLayout——完全自定義的Layout,添加刪除item,以及手勢識別
CircleLayout的例子稍微復雜一些,cell分布在圓周上,點擊cell的話會將其從collectionView中移出,點擊空白處會加入一個cell,加入和移出都有動畫效果。
這放在以前的話估計夠寫一陣子了,而得益于UICollectionView,基本只需要100來行代碼就可以搞定這一切,非常cheap。通過CircleLayout的實現,可以完整地看到自定義的layout的編寫流程,非常具有學習和借鑒的意義。
首先,布局準備中定義了一些之后計算所需要用到的參數。
123456789
-(void)prepareLayout{//和init相似,必須call super的prepareLayout以保證初始化正確[superprepareLayout];CGSizesize=self.collectionView.frame.size;_cellCount=[[selfcollectionView]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大小變化的需求時也可以自動適應變化。
然后,按照UICollectionViewLayout子類的要求,重載了所需要的方法:
123456789101112131415161718192021222324252627
//整個collectionView的內容大小就是collectionView的大小(沒有滾動)-(CGSize)collectionViewContentSize{return[selfcollectionView].frame.size;}//通過所在的indexPath確定位置。-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)path{UICollectionViewLayoutAttributes*attributes=[UICollectionViewLayoutAttributeslayoutAttributesForCellWithIndexPath: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));returnattributes;}//用來在一開始給出一套UICollectionViewLayoutAttributes-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{NSMutableArray*attributes=[NSMutableArrayarray];for(NSIntegeri=0;i<self.cellCount;i++){//這里利用了-layoutAttributesForItemAtIndexPath:來獲取attributesNSIndexPath*indexPath=[NSIndexPathindexPathForItem:iinSection:0];[attributesaddObject:[selflayoutAttributesForItemAtIndexPath:indexPath]];}returnattributes;}
現在已經得到了一個circle layout。為了實現cell的添加和刪除,需要為collectionView加上手勢識別,這個很簡單,在ViewController中:
12
UITapGestureRecognizer*tapRecognizer=[[UITapGestureRecognizeralloc]initWithTarget:selfaction:@selector(handleTapGesture:)];[self.collectionViewaddGestureRecognizer:tapRecognizer];
對應的處理方法handleTapGesture:為
1234567891011121314151617
-(void)handleTapGesture:(UITapGestureRecognizer*)sender{if(sender.state==UIGestureRecognizerStateEnded){CGPointinitialPinchPoint=[senderlocationInView:self.collectionView];NSIndexPath*tappedCellPath=[self.collectionViewindexPathForItemAtPoint:initialPinchPoint];//獲取點擊處的cell的indexPathif(tappedCellPath!=nil){//點擊處沒有cellself.cellCount=self.cellCount-1;[self.collectionViewperformBatchUpdates:^{[self.collectionViewdeleteItemsAtIndexPaths:[NSArrayarrayWithObject:tappedCellPath]];}completion:nil];}else{self.cellCount=self.cellCount+1;[self.collectionViewperformBatchUpdates:^{[self.collectionViewinsertItemsAtIndexPaths:[NSArrayarrayWithObject:[NSIndexPathindexPathForItem:0inSection:0]]];}completion:nil];}}}
performBatchUpdates:completion: 再次展示了block的強大的一面..這個方法可以用來對collectionView中的元素進行批量的插入,刪除,移動等操作,同時將觸發collectionView所對應的layout的對應的動畫。相應的動畫由layout中的下列四個方法來定義:
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
更正:正式版中API發生了變化(而且不止一次變化 initialLayoutAttributesForInsertedItemAtIndexPath:在正式版中已經被廢除。現在在insert或者delete之前,prepareForCollectionViewUpdates:會被調用,可以使用這個方法來完成添加/刪除的布局。關于更多這方面的內容以及新的示例demo,可以參看這篇博文(需要翻墻)。新的示例demo在Github上也有,鏈接
在CircleLayout中,實現了cell的動畫。
123456789101112131415161718
//插入前,cell在圓心位置,全透明-(UICollectionViewLayoutAttributes*)initialLayoutAttributesForInsertedItemAtIndexPath:(NSIndexPath*)itemIndexPath{UICollectionViewLayoutAttributes*attributes=[selflayoutAttributesForItemAtIndexPath:itemIndexPath];attributes.alpha=0.0;attributes.center=CGPointMake(_center.x,_center.y);returnattributes;}//刪除時,cell在圓心位置,全透明,且只有原來的1/10大-(UICollectionViewLayoutAttributes*)finalLayoutAttributesForDeletedItemAtIndexPath:(NSIndexPath*)itemIndexPath{UICollectionViewLayoutAttributes*attributes=[selflayoutAttributesForItemAtIndexPath:itemIndexPath];attributes.alpha=0.0;attributes.center=CGPointMake(_center.x,_center.y);attributes.transform3D=CATransform3DMakeScale(0.1,0.1,1.0);returnattributes;}
在插入或刪除時,將分別以插入前和刪除后的attributes和普通狀態下的attributes為基準,進行UIView的動畫過渡。而這一切并沒有很多代碼要寫,幾乎是free的,感謝蘋果…
有時候可能需要不同的布局,Apple也提供了方便的布局間切換的方法。直接更改collectionView的collectionViewLayout屬性可以立即切換布局。而如果通過setCollectionViewLayout:animated:,則可以在切換布局的同時,使用動畫來過渡。對于每一個cell,都將有對應的UIView動畫進行對應,又是一個接近free的特性。
對于我自己來說,UICollectionView可能是我轉向iOS6 SDK的最具有吸引力的特性之一,因為UIKit團隊的努力和CoreAnimation的成熟,使得創建一個漂亮優雅的UI變的越來越簡單了。可以斷言說UICollectionView在今后的ios開發中,一定會成為和UITableView一樣的強大和最常用的類之一。在iOS 6還未正式上市前,先對其特性進行一些學習,以期盡快能使用新特性來簡化開發流程,可以說是非常值得的。
謝謝!!!