//聯(lián)系人:石虎QQ: 1224614774昵稱:嗡嘛呢叭咪哄
這是博主的WWDC2012筆記系列中的一篇,完整的筆記列表可以參看這里瓣蛀。如果您是首次來(lái)到本站,也許您會(huì)有興趣通過(guò)RSS田巴,或者通過(guò)頁(yè)面左側(cè)的郵件訂閱的方式訂閱本站毁葱。
在上一篇UICollectionView的入門(mén)介紹中,大概地對(duì)iOS6新加入的強(qiáng)大的UICollectionView進(jìn)行了一些說(shuō)明杉女。在這篇博文中瞻讽,將結(jié)合WWDC2012 Session219:Advanced Collection View的內(nèi)容,對(duì)Collection View進(jìn)行一個(gè)深入的使用探討熏挎,并給出一個(gè)自定義的Demo速勇。
UICollectionView的結(jié)構(gòu)回顧
首先回顧一下Collection View的構(gòu)成,我們能看到的有三個(gè)部分:
Cells
Supplementary Views 追加視圖 (類似Header或者Footer)
Decoration Views 裝飾視圖 (用作背景展示)
而在表面下坎拐,由兩個(gè)方面對(duì)UICollectionView進(jìn)行支持烦磁。其中之一和tableView一樣,即提供數(shù)據(jù)的UICollectionViewDataSource以及處理用戶交互的UICollectionViewDelegate哼勇。另一方面都伪,對(duì)于cell的樣式和組織方式,由于collectionView比tableView要復(fù)雜得多积担,因此沒(méi)有按照類似于tableView的style的方式來(lái)定義陨晶,而是專門(mén)使用了一個(gè)類來(lái)對(duì)collectionView的布局和行為進(jìn)行描述,這就是UICollectionViewLayout帝璧。
這次的筆記將把重點(diǎn)放在UICollectionViewLayout上先誉,因?yàn)檫@不僅是collectionView和tableView的最重要求的區(qū)別,也是整個(gè)UICollectionView的精髓所在聋溜。
如果對(duì)UICollectionView的基本構(gòu)成要素和使用方法還不清楚的話谆膳,可以移步到我之前的一篇筆記:Session筆記——205 Introducing Collection Views中進(jìn)行一些了解。
UICollectionViewLayoutAttributes
UICollectionViewLayoutAttributes是一個(gè)非常重要的類撮躁,先來(lái)看看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的實(shí)例中包含了諸如邊框买雾,中心點(diǎn),大小杨帽,形狀漓穿,透明度,層次關(guān)系和是否隱藏等信息注盈。和DataSource的行為十分類似晃危,當(dāng)UICollectionView在獲取布局時(shí)將針對(duì)每一個(gè)indexPath的部件(包括cell,追加視圖和裝飾視圖)老客,向其上的UICollectionViewLayout實(shí)例詢問(wèn)該部件的布局信息(在這個(gè)層面上說(shuō)的話僚饭,實(shí)現(xiàn)一個(gè)UICollectionViewLayout的時(shí)候,其實(shí)很像是zap一個(gè)delegate胧砰,之后的例子中會(huì)很明顯地看出)鳍鸵,這個(gè)布局信息,就以UICollectionViewLayoutAttributes的實(shí)例的方式給出尉间。
UICollectionViewLayout的功能為向UICollectionView提供布局信息偿乖,不僅包括cell的布局信息,也包括追加視圖和裝飾視圖的布局信息哲嘲。實(shí)現(xiàn)一個(gè)自定義layout的常規(guī)做法是繼承UICollectionViewLayout類贪薪,然后重載下列方法:
-(CGSize)collectionViewContentSize
返回collectionView的內(nèi)容的尺寸
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
返回rect中的所有的元素的布局屬性
返回的是包含UICollectionViewLayoutAttributes的NSArray
UICollectionViewLayoutAttributes可以是cell,追加視圖或裝飾視圖的信息眠副,通過(guò)不同的UICollectionViewLayoutAttributes初始化方法可以得到不同類型的UICollectionViewLayoutAttributes:
layoutAttributesForCellWithIndexPath:
layoutAttributesForSupplementaryViewOfKind:withIndexPath:
layoutAttributesForDecorationViewOfKind:withIndexPath:
-(UICollectionViewLayoutAttributes)layoutAttributesForItemAtIndexPath:(NSIndexPath)indexPath
返回對(duì)應(yīng)于indexPath的位置的cell的布局屬性
-(UICollectionViewLayoutAttributes)layoutAttributesForSupplementaryViewOfKind:(NSString)kind atIndexPath:(NSIndexPath *)indexPath
返回對(duì)應(yīng)于indexPath的位置的追加視圖的布局屬性画切,如果沒(méi)有追加視圖可不重載
-(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString)decorationViewKind atIndexPath:(NSIndexPath)indexPath
返回對(duì)應(yīng)于indexPath的位置的裝飾視圖的布局屬性,如果沒(méi)有裝飾視圖可不重載
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
當(dāng)邊界發(fā)生改變時(shí)侦啸,是否應(yīng)該刷新布局槽唾。如果YES則在邊界變化(一般是scroll到其他地方)時(shí),將重新計(jì)算需要的布局信息光涂。
另外需要了解的是,在初始化一個(gè)UICollectionViewLayout實(shí)例后拧烦,會(huì)有一系列準(zhǔn)備方法被自動(dòng)調(diào)用忘闻,以保證layout實(shí)例的正確。
首先恋博,-(void)prepareLayout將被調(diào)用齐佳,默認(rèn)下該方法什么沒(méi)做,但是在自己的子類實(shí)現(xiàn)中债沮,一般在該方法中設(shè)定一些必要的layout的結(jié)構(gòu)和初始需要的參數(shù)等炼吴。
之后,-(CGSize) collectionViewContentSize將被調(diào)用疫衩,以確定collection應(yīng)該占據(jù)的尺寸硅蹦。注意這里的尺寸不是指可視部分的尺寸,而應(yīng)該是所有內(nèi)容所占的尺寸。collectionView的本質(zhì)是一個(gè)scrollView童芹,因此需要這個(gè)尺寸來(lái)配置滾動(dòng)行為涮瞻。
接下來(lái)-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被調(diào)用,這個(gè)沒(méi)什么值得多說(shuō)的假褪。初始的layout的外觀將由該方法返回的UICollectionViewLayoutAttributes來(lái)決定署咽。
另外,在需要更新layout時(shí)生音,需要給當(dāng)前l(fā)ayout發(fā)送 -invalidateLayout宁否,該消息會(huì)立即返回,并且預(yù)約在下一個(gè)loop的時(shí)候刷新當(dāng)前l(fā)ayout缀遍,這一點(diǎn)和UIView的setNeedsLayout方法十分類似慕匠。在-invalidateLayout后的下一個(gè)collectionView的刷新loop中,又會(huì)從prepareLayout開(kāi)始瑟由,依次再調(diào)用-collectionViewContentSize和-layoutAttributesForElementsInRect來(lái)生成更新后的布局絮重。
說(shuō)了那么多,其實(shí)還是Demo最能解決問(wèn)題歹苦。Apple官方給了一個(gè)flow layout和一個(gè)circle layout的例子青伤,都很經(jīng)典,需要的同學(xué)可以從這里下載殴瘦。
LineLayout——對(duì)于個(gè)別UICollectionViewLayoutAttributes的調(diào)整
先看LineLayout狠角,它繼承了UICollectionViewFlowLayout這個(gè)Apple提供的基本的布局。它主要實(shí)現(xiàn)了單行布局蚪腋,自動(dòng)對(duì)齊到網(wǎng)格以及當(dāng)前網(wǎng)格cell放大三個(gè)特性丰歌。
先看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); 確定了縮進(jìn),此處為上方和下方各縮進(jìn)200個(gè)point屉凯。由于cell的size已經(jīng)定義了為200x200立帖,因此屏幕上在縮進(jìn)后就只有一排item的空間了。
self.minimumLineSpacing = 50.0; 這個(gè)定義了每個(gè)item在水平方向上的最小間距悠砚。
UICollectionViewFlowLayout是Apple為我們準(zhǔn)備的開(kāi)袋即食的現(xiàn)成布局晓勇,因此之前提到的幾個(gè)必須重載的方法中需要我們操心的很少,即使完全不重載它們灌旧,現(xiàn)在也可以得到一個(gè)不錯(cuò)的線狀一行的gridview了绑咱。而我們的LineLayout通過(guò)重載父類方法后,可以實(shí)現(xiàn)一些新特性枢泰,比如這里的動(dòng)對(duì)齊到網(wǎng)格以及當(dāng)前網(wǎng)格cell放大描融。
自動(dòng)對(duì)齊到網(wǎng)格
1234567891011121314151617
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffsetwithScrollingVelocity:(CGPoint)velocity{//proposedContentOffset是沒(méi)有對(duì)齊到網(wǎng)格時(shí)本來(lái)應(yīng)該停下的位置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];//對(duì)當(dāng)前屏幕中的UICollectionViewLayoutAttributes逐個(gè)與屏幕中心進(jìn)行比較,找出最接近中心的一個(gè)for(UICollectionViewLayoutAttributes*layoutAttributesinarray){CGFloatitemHorizontalCenter=layoutAttributes.center.x;if(ABS(itemHorizontalCenter-horizontalCenter)<ABS(offsetAdjustment)){offsetAdjustment=itemHorizontalCenter-horizontalCenter;}}returnCGPointMake(proposedContentOffset.x+offsetAdjustment,proposedContentOffset.y);}
當(dāng)前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;}
對(duì)于個(gè)別UICollectionViewLayoutAttributes進(jìn)行調(diào)整衡蚂,以達(dá)到滿足設(shè)計(jì)需求是UICollectionView使用中的一種思路窿克。在根據(jù)位置提供不同layout屬性的時(shí)候骏庸,需要記得讓-shouldInvalidateLayoutForBoundsChange:返回YES,這樣當(dāng)邊界改變的時(shí)候让歼,-invalidateLayout會(huì)自動(dòng)被發(fā)送敞恋,才能讓layout得到刷新。
CircleLayout——完全自定義的Layout谋右,添加刪除item硬猫,以及手勢(shì)識(shí)別
CircleLayout的例子稍微復(fù)雜一些,cell分布在圓周上改执,點(diǎn)擊cell的話會(huì)將其從collectionView中移出啸蜜,點(diǎn)擊空白處會(huì)加入一個(gè)cell,加入和移出都有動(dòng)畫(huà)效果辈挂。
這放在以前的話估計(jì)夠?qū)懸魂囎恿顺暮幔靡嬗赨ICollectionView,基本只需要100來(lái)行代碼就可以搞定這一切终蒂,非常cheap蜂林。通過(guò)CircleLayout的實(shí)現(xiàn),可以完整地看到自定義的layout的編寫(xiě)流程拇泣,非常具有學(xué)習(xí)和借鑒的意義噪叙。
首先,布局準(zhǔn)備中定義了一些之后計(jì)算所需要用到的參數(shù)霉翔。
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;}
其實(shí)對(duì)于一個(gè)size不變的collectionView來(lái)說(shuō),除了_cellCount之外的中心和半徑的定義也可以扔到init里去做债朵,但是顯然在prepareLayout里做的話具有更大的靈活性子眶。因?yàn)槊看沃匦陆o出layout時(shí)都會(huì)調(diào)用prepareLayout,這樣在以后如果有collectionView大小變化的需求時(shí)也可以自動(dòng)適應(yīng)變化序芦。
然后臭杰,按照UICollectionViewLayout子類的要求,重載了所需要的方法:
123456789101112131415161718192021222324252627
//整個(gè)collectionView的內(nèi)容大小就是collectionView的大醒柚小(沒(méi)有滾動(dòng))-(CGSize)collectionViewContentSize{return[selfcollectionView].frame.size;}//通過(guò)所在的indexPath確定位置硅卢。-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)path{UICollectionViewLayoutAttributes*attributes=[UICollectionViewLayoutAttributeslayoutAttributesForCellWithIndexPath:path];//生成空白的attributes對(duì)象,其中只記錄了類型是cell以及對(duì)應(yīng)的位置是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;}//用來(lái)在一開(kāi)始給出一套UICollectionViewLayoutAttributes-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{NSMutableArray*attributes=[NSMutableArrayarray];for(NSIntegeri=0;i<self.cellCount;i++){//這里利用了-layoutAttributesForItemAtIndexPath:來(lái)獲取attributesNSIndexPath*indexPath=[NSIndexPathindexPathForItem:iinSection:0];[attributesaddObject:[selflayoutAttributesForItemAtIndexPath:indexPath]];}returnattributes;}
現(xiàn)在已經(jīng)得到了一個(gè)circle layout藏杖。為了實(shí)現(xiàn)cell的添加和刪除,需要為collectionView加上手勢(shì)識(shí)別脉顿,這個(gè)很簡(jiǎn)單蝌麸,在ViewController中:
12
UITapGestureRecognizer*tapRecognizer=[[UITapGestureRecognizeralloc]initWithTarget:selfaction:@selector(handleTapGesture:)];[self.collectionViewaddGestureRecognizer:tapRecognizer];
對(duì)應(yīng)的處理方法handleTapGesture:為
1234567891011121314151617
-(void)handleTapGesture:(UITapGestureRecognizer*)sender{if(sender.state==UIGestureRecognizerStateEnded){CGPointinitialPinchPoint=[senderlocationInView:self.collectionView];NSIndexPath*tappedCellPath=[self.collectionViewindexPathForItemAtPoint:initialPinchPoint];//獲取點(diǎn)擊處的cell的indexPathif(tappedCellPath!=nil){//點(diǎn)擊處沒(méi)有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的強(qiáng)大的一面..這個(gè)方法可以用來(lái)對(duì)collectionView中的元素進(jìn)行批量的插入,刪除艾疟,移動(dòng)等操作来吩,同時(shí)將觸發(fā)collectionView所對(duì)應(yīng)的layout的對(duì)應(yīng)的動(dòng)畫(huà)敢辩。相應(yīng)的動(dòng)畫(huà)由layout中的下列四個(gè)方法來(lái)定義:
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
更正:正式版中API發(fā)生了變化(而且不止一次變化 initialLayoutAttributesForInsertedItemAtIndexPath:在正式版中已經(jīng)被廢除。現(xiàn)在在insert或者delete之前弟疆,prepareForCollectionViewUpdates:會(huì)被調(diào)用戚长,可以使用這個(gè)方法來(lái)完成添加/刪除的布局。關(guān)于更多這方面的內(nèi)容以及新的示例demo怠苔,可以參看這篇博文(需要翻墻)同廉。新的示例demo在Github上也有,鏈接
在CircleLayout中柑司,實(shí)現(xiàn)了cell的動(dòng)畫(huà)迫肖。
123456789101112131415161718
//插入前,cell在圓心位置攒驰,全透明-(UICollectionViewLayoutAttributes*)initialLayoutAttributesForInsertedItemAtIndexPath:(NSIndexPath*)itemIndexPath{UICollectionViewLayoutAttributes*attributes=[selflayoutAttributesForItemAtIndexPath:itemIndexPath];attributes.alpha=0.0;attributes.center=CGPointMake(_center.x,_center.y);returnattributes;}//刪除時(shí)蟆湖,cell在圓心位置,全透明玻粪,且只有原來(lái)的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;}
在插入或刪除時(shí)隅津,將分別以插入前和刪除后的attributes和普通狀態(tài)下的attributes為基準(zhǔn),進(jìn)行UIView的動(dòng)畫(huà)過(guò)渡劲室。而這一切并沒(méi)有很多代碼要寫(xiě)伦仍,幾乎是free的,感謝蘋(píng)果…
有時(shí)候可能需要不同的布局痹籍,Apple也提供了方便的布局間切換的方法呢铆。直接更改collectionView的collectionViewLayout屬性可以立即切換布局。而如果通過(guò)setCollectionViewLayout:animated:蹲缠,則可以在切換布局的同時(shí)棺克,使用動(dòng)畫(huà)來(lái)過(guò)渡。對(duì)于每一個(gè)cell线定,都將有對(duì)應(yīng)的UIView動(dòng)畫(huà)進(jìn)行對(duì)應(yīng)娜谊,又是一個(gè)接近free的特性。
對(duì)于我自己來(lái)說(shuō)斤讥,UICollectionView可能是我轉(zhuǎn)向iOS6 SDK的最具有吸引力的特性之一纱皆,因?yàn)閁IKit團(tuán)隊(duì)的努力和CoreAnimation的成熟,使得創(chuàng)建一個(gè)漂亮優(yōu)雅的UI變的越來(lái)越簡(jiǎn)單了芭商∨刹荩可以斷言說(shuō)UICollectionView在今后的ios開(kāi)發(fā)中,一定會(huì)成為和UITableView一樣的強(qiáng)大和最常用的類之一铛楣。在iOS 6還未正式上市前近迁,先對(duì)其特性進(jìn)行一些學(xué)習(xí),以期盡快能使用新特性來(lái)簡(jiǎn)化開(kāi)發(fā)流程簸州,可以說(shuō)是非常值得的鉴竭。
謝謝!!!