注:
本文翻譯自 《iOS UICollectionView The Complete Guide 2nd Edition》
使用的翻譯工具:https://www.deepl.com/translator
你現(xiàn)在已經(jīng)掌握了使用 UICollectionView
向用戶顯示自定義內(nèi)容的技巧,可以顯示單元格以及輔助視圖茅主。到目前為止产阱,我們一直關(guān)注實際的內(nèi)容定铜,而不是如何在屏幕上組織內(nèi)容。本章將探討 UICollectionView
是如何被設(shè)計成使用 UICollectionViewLayout
來組織其內(nèi)容的说敏。我們仔細研究了 UICollectionViewFlowLayout
,以及如何通過子類化它來獲得大量的可定制性,而不需要大量的額外工作鲁僚。當我們以簡短的歷史課結(jié)束,我們會探索 UITableView
以及它與 UICollectionView
的關(guān)系。
什么是布局
UICollectionViewLayout
是一個抽象類冰沙,它不應(yīng)該被直接創(chuàng)建侨艾,它存在的唯一目的是被子類化。每個集合視圖都有一個與之相關(guān)聯(lián)的布局對象拓挥,它的工作是將內(nèi)容布局出來唠梨。布局對象并不關(guān)心其布局的視圖中所包含的數(shù)據(jù),它只關(guān)心向用戶展示的布局侥啤。
UICollectionViewFlow
是一個直接的子類当叭,它以基于行的、分塊的方式來布局內(nèi)容盖灸。我們已經(jīng)看到了UICollectionViewFlowLayout
最基本的形式蚁鳖,一個網(wǎng)格。我們用本章的其余部分來探索一個簡單的流式布局子類給你作為一個開發(fā)者帶來的力量赁炎。如果你知道把它放在哪里的話醉箕,你可以用很少的代碼來創(chuàng)建令人震驚的布局。
子類布局對象有一些職責徙垫。集合視圖依靠這個子類布局對象來告訴它如何顯示其單元格讥裤。這是一個關(guān)鍵的概念。布局內(nèi)容并不是通過創(chuàng)建一個 UICollectionView
的子類來實現(xiàn)的姻报。雖然這是在子類UIScrollView
時布局子視圖的常見模式己英,但除非絕對必要,我們要避免創(chuàng)建 UICollectionView
子類逗抑。
所以剧辐,集合視圖會向其布局對象詢問如何布局其內(nèi)容的線索。當一個集合視圖向用戶顯示內(nèi)容時邮府,實際發(fā)生的事件順序是什么荧关?
首先,集合視圖詢問其數(shù)據(jù)源褂傀,以獲得關(guān)于要向用戶顯示的內(nèi)容的信息忍啤。這包括要顯示多少組數(shù)據(jù)、每組數(shù)據(jù)要顯示的單元格仙辟、輔助視圖的數(shù)量同波。
接下來,集合視圖從其布局對象中收集有關(guān)如何顯示單元格叠国、輔助視圖和裝飾視圖的信息未檩。這些信息存儲在一個名為 UICollectionViewLayoutAttributes
的類的實例對象中。
最后粟焊,集合視圖將有關(guān)布局的信息轉(zhuǎn)發(fā)給單元格冤狡、輔助視圖和裝飾視圖孙蒙。這些類中的每一個類都負責使用它所得到的信息將這些布局屬性應(yīng)用到自己身上。推遲到父類的實現(xiàn)悲雳,或者完全省略一個實現(xiàn)挎峦,將確保已經(jīng)由集合視圖處理的布局屬性(如框架)得到應(yīng)用。你的實現(xiàn)應(yīng)該集中在你添加的任何自定義屬性上(但后面會有更多的介紹)合瓢。
每當當前布局失效時坦胶,就會發(fā)生這些步驟,你可以通過在布局對象上調(diào)用 invalidateLayout
來強制執(zhí)行布局更新晴楔。
現(xiàn)在你已經(jīng)知道了布局內(nèi)容時使用的不同類:
-
UICollectionView
是向用戶展示內(nèi)容的視圖顿苇; -
UICollectionViewCell
負責向用戶展示一個單元格的內(nèi)容; -
UICollectionViewLayout
確定單元格的布局位置信息滥崩,并將這些信息返回給集合視圖岖圈; -
UICollectionViewLayoutAttributes
,它是一個布局存儲信息的類钙皮,要將這些信息調(diào)配給單元格蜂科、輔助視圖和裝飾視圖。
如果回過頭來看這些類短条,就會發(fā)現(xiàn)有一個明顯的劃分导匣,哪些是參與數(shù)據(jù)和自身布局的,哪些是只負責布局的茸时。圖4.1顯示了這種劃分贡定。UICollectionView
從橙色框中的類中收集數(shù)據(jù)信息,并將其與藍色框中的類的布局信息相結(jié)合可都。
請注意缓待,布局對象對集合視圖的委托對象有一個間接的引用。這個連接可以被布局對象用來詢問委托對象關(guān)于特定項目布局的信息渠牲。例如旋炒,UICollectionViewDelegateFlowLayout
協(xié)議擴展了UICollectionViewDelegate
,并被 UICollectionViewFlowLayout
用來詢問委托對象關(guān)于特定 item 的布局信息签杈。這個話題很復雜瘫镇,但你已經(jīng)在上一章看到了一個例子,當委托對象為不同的 item 指定單獨的 size 時答姥。你將在后面看到一個進一步擴展這個功能的例子铣除。
我們已經(jīng)介紹了基礎(chǔ)知識:什么是布局,它有什么作用鹦付,以及它如何與集合視圖架構(gòu)的其他部分進行交互尚粘。到目前為止,這已經(jīng)是非常學術(shù)性的內(nèi)容了敲长。讓我們來看看一些代碼背苦。
創(chuàng)建 UICollectionViewFlowLayout
子類
我們已經(jīng)看到很多復雜的行為和布局是使用內(nèi)置的 UICollectionViewFlowLayout
生成的互捌,那么為什么會選擇將其子類化呢?原因有很多行剂。
- 要修改你的子類的布局的屬性,這超出了委托方法所能實現(xiàn)的范圍钳降;
- 在你的布局中加入裝飾視圖厚宰;
- 增加新的輔助視圖;
- 要擴展
UICollectionViewLayoutAttributes
遂填,為你的布局類添加新的項目屬性來管理铲觉; - 要添加手勢支持;
- 要自定義插入吓坚、更新和刪除更新到集合視圖的動畫撵幽。
除了在第 6 章 "為 UICollectionView
添加交互性 "中涵蓋的手勢支持外,我們將針對每個原因的子類流式布局看代碼示例礁击。
讓我們回顧 Survey 示例——它的代碼在 Better Survey 中盐杂。有幾種方法可以讓它變得更好,第一種方法如圖 4.2 所示哆窿。因為不是所有的單元格都有相同的大小链烈,所以單元格不會再垂直對齊。開箱即用挚躯,UICollectionViewFlowLayout
并沒有提供對那種 "均勻間隔 "感覺的支持强衡,我認為這種感覺在這里會更好。幸運的是码荔,我們想要的東西屬于 "基于行的漩勤,打破布局 " 的流式布局,所以我想我們可以通過創(chuàng)建一個 UICollectionViewFlowLayout
子類的方式來實現(xiàn)我們想要的視覺效果缩搅。
在 Xcode 中創(chuàng)建一個名為 AFCollectionViewFlowLayout
的新類越败,它是 UICollectionViewFlowLayout
的子類對象。接下來誉己,我們可以把視圖控制器中的很多布局邏輯代碼移動到該布局類中眉尸。
#import <UIKit/UIKit.h>
#define kMaxItemDimension 100.0f
#define kMaxItemSize CGSizeMake(kMaxItemDimension, kMaxItemDimension)
extern NSString * const AFCollectionViewFlowLayoutBackgroundDecoration;
@interface AFCollectionViewFlowLayout : UICollectionViewFlowLayout
@end
可以看到,我們已經(jīng)將單元格最大尺寸的宏定義(kMaxItemSize
)移動到布局的頭文件中巨双。這是個(比把它放在視圖控制器實現(xiàn)文件中)更合適的地方噪猾。
接下來,我們將實現(xiàn) init
方法筑累,并在里面設(shè)置布局參數(shù):
-(instancetype)init {
if (!(self = [super init])) return nil;
// 在初始化方法中設(shè)置默認布局參數(shù)
self.sectionInset = UIEdgeInsetsMake(15.0f, 5.0f, 15.0f, 5.0f);
self.minimumInteritemSpacing = 5.0f;
self.minimumLineSpacing = 5.0f;
self.itemSize = kMaxItemSize;
self.headerReferenceSize = CGSizeMake(60, 70);
return self;
}
最后袱蜡,我們需要更新并創(chuàng)建視圖控制器中的布局對象。使用 #import
導入AFCollectionViewFlowLayout
頭文件慢宗,將布局和集合視圖的創(chuàng)建方式改為清單 4.3 所示的代碼坪蚁。
// 創(chuàng)建一個基礎(chǔ)流式布局奔穿,以自適應(yīng)縱向的三列
AFCollectionViewFlowLayout *surveyFlowLayout = [[AFCollectionViewFlowLayout alloc] init];
// 用自定義的流式布局創(chuàng)建一個新的集合視圖,并設(shè)置委托對象和數(shù)據(jù)源對象
UICollectionView *surveyCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:surveyFlowLayout];
通過將布局參數(shù)的相關(guān)設(shè)置移動到布局對象本身的初始化方法中敏晤,我們在視圖控制器中寫的代碼就少了很多贱田。此外,如果我們重用該布局嘴脾,我們就不會在兩個地方編寫重復的代碼男摧。重用這個布局的視圖控制器總是可以進一步自定義布局屬性,但他們不必這樣做译打。這是你在編寫自定義布局時的最佳實踐耗拓。
接下來,需要在我們的 UICollectionViewFlowLayout
子類中重寫兩個方法奏司,當集合視圖在布局其單元格乔询、輔助視圖和裝飾視圖時,這些方法將被調(diào)用韵洋。這兩個方法是 layoutAttributesForElementsInRect:
和 layoutAttributesForItemAtIndexPath:
竿刁。我們還要創(chuàng)建第三個私有方法,叫做 applyLayoutAttributes:
麻献,我們在后面討論们妥。這兩個被覆蓋的方法都會調(diào)用這個自定義方法(見清單 4.4)。
/**
該方法返回一個包含所有布局信息 UICollectionViewLayoutAttributes 的數(shù)組勉吻。
我們通過父類方法 [super layoutAttributesForElementsInRect:rect] 先創(chuàng)建了一個正常情況下的所有屬性的數(shù)組监婶。
這個父類方法默認情況下,只會創(chuàng)建在 rect 范圍內(nèi)的視圖的布局屬性齿桃。
所以惑惶,如果你想把原來不會被顯示的視圖也顯示出來的話,你就不得不自己把所有布局屬性都創(chuàng)建出來短纵,放入數(shù)組中带污。
*/
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];
// 該數(shù)組中存放我們在每個 section 中新增的「裝飾視圖」布局參數(shù)
NSMutableArray *newAttributesArray = [NSMutableArray array];
for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
[self applyLayoutAttributes:attributes];
}
attributesArray = [attributesArray arrayByAddingObjectsFromArray:newAttributesArray];
return attributesArray;
}
// 布局 item
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForItemAtIndexPath:indexPath];
[self applyLayoutAttributes:attributes];
return attributes;
}
這兩個方法所做的第一件事就是調(diào)用它們父類的實現(xiàn)。通過這樣做香到,我們免費獲得了所有的 UICollectionViewFlowLayout
默認行為鱼冀。在我們檢索到默認屬性后,再調(diào)整各個 item 的布局悠就。
現(xiàn)在我們來看看 applyLayoutAttributes:
方法千绪。我們首先檢查布局屬性的 representedElementKind
屬性。對于普通的 UICollectionViewCell
來說梗脾,這將是 nil荸型。否則,它將是集合視圖注冊的輔助視圖類型炸茧;在我們的例子中瑞妇,它將是UICollectionElementKindSectionHeader
稿静。還有一點值得記住,center 和 size 分別定義了一個 item 的 position 和 size辕狰。當計算這些時改备,你可能最終會在半像素上渲染視圖,使它們變得模糊不清柳琢。frame 屬性是一種方便的方法绍妨,用于訪問布局屬性的大小和中心。通過將框架設(shè)置為自身的 CGRectIntegral
(見清單4.5)柬脸,我們可以確保視圖不會呈現(xiàn)在像素邊界上。
// 修改并更新每一個 item 的位置
-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes {
// 對于一個普通的 UICollectionViewCell 來說毙驯,它的 representedElementKind 值為 nil
// 檢查 representedElementKind 是否為 nil国旷,表明這是一個單元格隆圆,而不是一個 header view 或裝飾視圖。
if (attributes.representedElementKind == nil) {
CGFloat width = [self collectionViewContentSize].width;
CGFloat leftMargin = [self sectionInset].left;
CGFloat rightMargin = [self sectionInset].right;
NSUInteger itemsInSection = [[self collectionView] numberOfItemsInSection:attributes.indexPath.section];
// xPosition 指單元格的 centerX
CGFloat firstXPosition = (width - (leftMargin + rightMargin)) / (2 * itemsInSection);
CGFloat xPosition = firstXPosition + (2*firstXPosition*attributes.indexPath.item);
attributes.center = CGPointMake(leftMargin + xPosition, attributes.center.y);
attributes.frame = CGRectIntegral(attributes.frame);
}
}
清單4.5 只是圖4.3 中所列公式的編輯版本。它已被概括為允許每行有任意數(shù)量的項目岳锁,而不是只有三個。
啊崭歧,你知道這本電子書最終會有一些數(shù)學的內(nèi)容! 但是胧华,其實并沒有那么復雜。
如果我們再運行這個應(yīng)用程序序愚,我們會看到單元格是均勻分布的憔披,如圖4.4所示。
現(xiàn)在我們已經(jīng)把我們的單元格排列成一個漂亮的網(wǎng)格模式爸吮,讓我們添加一個裝飾視圖芬膝。裝飾視圖是對 UICollectionView
的數(shù)據(jù)驅(qū)動內(nèi)容的視覺補充。它們并不顯示單元格的信息形娇;相反锰霜,它們伴隨著單元格的視覺效果:設(shè)計師最好的朋友。
我不是設(shè)計師桐早,但我已經(jīng)成功地想出了一個文件夾的想法癣缅。我們的應(yīng)用要炫耀這股 "扁平化設(shè)計 "的熱潮,將我們的照片擺放在一個三環(huán)的文件夾上面哄酝。我拍了一張文件夾的照片友存,然后把它拉長。我們要讓這個裝飾視圖鋪在每一排照片的后面炫七。
因為裝飾視圖不是數(shù)據(jù)驅(qū)動的爬立,所以不會向視圖控制器添加任何代碼。相反万哪,裝飾視圖的所有代碼都將存在于我們的 AFCollectionViewFlowLayout
和 UICollectionReusableView
的一個子類中侠驯。
這個類 UICollectionReusableView
是 AFCollectionHeaderView
甚至UICollectionViewCell
的父類抡秆。它提供了重用集合視圖中任何特定視圖的通用邏輯,其中包括單元格吟策、補充視圖和裝飾視圖儒士。因為這些類可以重用,我們可以把已經(jīng)學到的關(guān)于重用的知識應(yīng)用到裝飾視圖中¢菁幔現(xiàn)在就讓我們這樣做着撩。
創(chuàng)建一個新的類,父類是 UICollectionReusableView
匾委。我把我的類叫做 AFDecorationView
拖叙。它沒有任何屬性,而且它的實現(xiàn)看起來相當無聊(見清單4.6)赂乐。
#import "AFDecorationView.h"
@implementation AFDecorationView
{
UIImageView *binderImageView;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (!(self = [super initWithFrame:frame])) return nil;
binderImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"binder"]];
binderImageView.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));
binderImageView.contentMode = UIViewContentModeScaleToFill;
binderImageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self addSubview:binderImageView];
return self;
}
@end
這個類所做的就是薯鳍,當初始化時,在它的視圖層次結(jié)構(gòu)中添加一個UIImageView
挨措,里面有我們的 "binder "圖像挖滤。沒有必要覆蓋 prepareForReuse
方法,因為在我們的裝飾視圖中沒有特定的數(shù)據(jù)內(nèi)容浅役。
現(xiàn)在我們已經(jīng)創(chuàng)建了裝飾視圖子類斩松,讓我們把它添加到集合視圖中。這比 header 視圖要棘手一些觉既,因為 UICollectionView
并沒有為我們內(nèi)置任何內(nèi)容惧盹,我們需要自己構(gòu)建一切。
將裝飾視圖的頭文件導入到布局子類中奋救。修改 layoutAttributesForElementsInRect:
方法的實現(xiàn)岭参,使其看起來像清單4.7。
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];
// 該數(shù)組中存放我們在每個 section 中新增的「裝飾視圖」布局參數(shù)
NSMutableArray *newAttributesArray = [NSMutableArray array];
for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
[self applyLayoutAttributes:attributes];
// 默認情況下尝艘,「裝飾視圖」不會被顯示演侯,所以需要創(chuàng)建并添加「裝飾視圖」的布局屬性
// MARK: 添加自定義的裝飾視圖
if (attributes.representedElementCategory == UICollectionElementCategorySupplementaryView) {
UICollectionViewLayoutAttributes *newAttributes = [self layoutAttributesForDecorationViewOfKind:AFCollectionViewFlowLayoutBackgroundDecoration atIndexPath:attributes.indexPath];
[newAttributesArray addObject:newAttributes];
}
}
attributesArray = [attributesArray arrayByAddingObjectsFromArray:newAttributesArray];
return attributesArray;
}
增加了檢查布局屬性的元素類型的 if 語句。我們想在每個部分添加一個裝飾視圖背亥,而每個部分只有一個 header秒际,所以我們將搭載這個邏輯來添加我們的輔助視圖。
代碼本身可能看起來有點奇怪狡汉。請記住娄徊,layoutAttributesForElementsInRect:
是為所有類型的元素調(diào)用的,而不僅僅是單元格盾戴。因此寄锐,當它被調(diào)用到我們的 header 視圖時,我們的 if 語句評估為 YES,我們就會創(chuàng)建一個新的布局屬性橄仆。我們返回的數(shù)組將包含這個新屬性剩膘。
接下來,我們需要為 layoutAttributesForDecorationViewOfKind:atIndexPath:
實現(xiàn)一個方法盆顾,因為默認的實現(xiàn)會返回 nil怠褐,當我們試圖將它添加到我們的可變字典中時,我們的應(yīng)用程序會崩潰您宪。
我們需要實現(xiàn)一個方法奈懒,該方法將創(chuàng)建一個新的 UICollectionViewLayoutAttributes
對象,并自定義它的屬性宪巨,以便裝飾視圖將適合我們的單元格內(nèi)容后面(見清單4.8)磷杏。
// 裝飾視圖布局
-(UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:decorationViewKind withIndexPath:indexPath];
if ([decorationViewKind isEqualToString:AFCollectionViewFlowLayoutBackgroundDecoration]) {
UICollectionViewLayoutAttributes *tallestCellAttributes;
NSInteger numberOfCellsInSection = [self.collectionView numberOfItemsInSection:indexPath.section];
for (NSInteger i = 0; i < numberOfCellsInSection; i++) {
NSIndexPath *cellIndexPath = [NSIndexPath indexPathForItem:i inSection:indexPath.section];
UICollectionViewLayoutAttributes *cellAttribtes = [self layoutAttributesForItemAtIndexPath:cellIndexPath];
if (CGRectGetHeight(cellAttribtes.frame) > CGRectGetHeight(tallestCellAttributes.frame)) {
tallestCellAttributes = cellAttribtes;
}
}
CGFloat decorationViewHeight = CGRectGetHeight(tallestCellAttributes.frame) + self.headerReferenceSize.height;
layoutAttributes.size = CGSizeMake([self collectionViewContentSize].width, decorationViewHeight);
layoutAttributes.center = CGPointMake([self collectionViewContentSize].width / 2.0f, tallestCellAttributes.center.y);
layoutAttributes.frame = CGRectIntegral(layoutAttributes.frame);
/**
默認情況下,單元格的 zIndex 值為 0捏卓,
將裝飾視圖的 zIndex 值設(shè)置為 -1茴丰,可以將「裝飾視圖」顯示在單元格的視圖層次后面。
*/
layoutAttributes.zIndex = -1;
}
return layoutAttributes;
}
本示例使用方法 layoutAttributesForDecorationViewOfKind:withIndexPath:
創(chuàng)建一個新的 UICollectionViewLayoutAttributes
對象天吓。然后,它根據(jù)我們要找的東西來定制屬性中的屬性峦椰。我們希望我們的裝飾視圖以其部分中最高的項目為垂直中心龄寞,所以我們需要循環(huán)處理每一個項目。幸運的是汤功,檢索這些屬性的邏輯已經(jīng)在 layoutAttributesForItemAtIndexPath:
中實現(xiàn)了物邑。當我們向我們的超級類詢問給定單元格的屬性時,它會查詢集合視圖委托的大刑辖稹(代碼我們已經(jīng)寫好了)色解。
我們可以利用這個現(xiàn)有的功能來處理繁重的工作。我們并沒有計算裝飾視圖的中心餐茵,實際上科阎,我們只是依靠最高物品的垂直中心,它已經(jīng)為我們計算好了忿族。萬歲!
所以锣笨,在我們定義了裝飾視圖的大小和高度之后,我們需要設(shè)置它的 zIndex
屬性道批。這將告訴集合視圖以何種順序呈現(xiàn)其項目错英。重疊但具有相同 zIndex
的項目有一個未定義的渲染順序。我們希望裝飾視圖渲染在所有單元格后面隆豹,而這些單元格的默認 zIndex
為 0椭岩,所以我們將裝飾視圖的 zIndex
設(shè) 置為 -1。
我們需要做的唯一一件事就是將我們的裝飾視圖類注冊到布局類中(見清單 4.9)。我們將在AFCollectionViewFlowLayout
的init
方法中添加下面高亮顯示的一行判哥。
-(instancetype)init {
if (!(self = [super init])) return nil;
// 在初始化方法中設(shè)置默認布局參數(shù)
self.sectionInset = UIEdgeInsetsMake(15.0f, 5.0f, 15.0f, 5.0f);
self.minimumInteritemSpacing = 5.0f;
self.minimumLineSpacing = 5.0f;
self.itemSize = kMaxItemSize;
self.headerReferenceSize = CGSizeMake(60, 70);
// !!!: 注冊裝飾視圖
[self registerClass:[AFDecorationView class] forDecorationViewOfKind:AFCollectionViewFlowLayoutBackgroundDecoration];
return self;
}
Surprise献雅!圖 4.5 顯示姨伟,我們幾乎達到了目的惩琉。最后,我認為這個演示程序可以使用一些漂亮的動畫夺荒。UICollectionViewLayout
中已經(jīng)內(nèi)置了對動畫的支持瞒渠,我們只需要實現(xiàn)一些方法。
當一個新的 item 被添加或更新到集合視圖中時技扼,initialLayoutAttributesForAppearingItemAtIndexPath:
方法就會被調(diào)用伍玖。我們可以通過它在動畫開始時為 item 提供初始布局屬性,集合視圖將把可動畫的屬性剿吻,如 frame
和 alpha
窍箍,插值到它們的正常位置。還有一個對應(yīng)的方法叫做finalLayoutAttributesForDisappearingItemAtIndexPath:
用于通過動畫方式從集合視圖中移除 item丽旅。
不過我們可以動畫的不僅僅是 item 元素椰棘。輔助視圖和裝飾視圖都有相應(yīng)的出現(xiàn)/消失方法。UICollectionViewLayout
的默認實現(xiàn)返回 nil
榄笙,表示簡單的交叉淡化(crossfade)動畫邪狞。我們也可以返回 nil
來使用交叉淡化動畫。
剩下的問題是茅撞,當我們插入一個新的 section 時帆卓,其他 section 也會被重新加載。這會導致不僅僅是出現(xiàn)的 secion 有動畫米丘。我們還需要限制哪些 section 會執(zhí)行動畫剑令。
在對集合視圖進行任何更新之前,prepareForCollectionViewUpdates:
被調(diào)用拄查,其參數(shù)是一個 UICcollectionViewUpdateItem
對象數(shù)組吁津。這些是即將發(fā)生的更新。在它們完成后靶累,調(diào)用 finalizeCollectionViewUpdates
腺毫。這些都是成對的。我們將創(chuàng)建一個實例變量NSMutableSet
來獲取正在插入的 section挣柬。我們使用 set 是因為它具有恒時查找功能(見清單4.10)潮酒。
@implementation AFCollectionViewFlowLayout
{
NSMutableSet *insertedSectionSet;
}
-(instancetype)init {
if (!(self = [super init])) return nil;
// 在初始化方法中設(shè)置默認布局參數(shù)
self.sectionInset = UIEdgeInsetsMake(15.0f, 5.0f, 15.0f, 5.0f);
self.minimumInteritemSpacing = 5.0f;
self.minimumLineSpacing = 5.0f;
self.itemSize = kMaxItemSize;
self.headerReferenceSize = CGSizeMake(60, 70);
// !!!: 注冊裝飾視圖
[self registerClass:[AFDecorationView class] forDecorationViewOfKind:AFCollectionViewFlowLayoutBackgroundDecoration];
insertedSectionSet = [NSMutableSet set];
return self;
}
現(xiàn)在我們只需要實現(xiàn) prepareForCollectionViewUpdates:
和 finalizeCollectionViewUpdates
方法來更新集合視圖。對于這些方法邪蛔,始終調(diào)用你的 super
實現(xiàn)是非常重要的(見清單4.11)急黎。
#pragma mark Animation Support
-(void)prepareForCollectionViewUpdates:(NSArray *)updateItems {
[super prepareForCollectionViewUpdates:updateItems];
[updateItems enumerateObjectsUsingBlock:^(UICollectionViewUpdateItem *updateItem, NSUInteger idx, BOOL *stop) {
// 如果當前的 item 動作為 Insert,則記錄到 NSMutableSet 集合中
if (updateItem.updateAction == UICollectionUpdateActionInsert) {
[insertedSectionSet addObject:@(updateItem.indexPathAfterUpdate.section)];
}
}];
}
-(void)finalizeCollectionViewUpdates {
[super finalizeCollectionViewUpdates];
// 當更新完成后,從可變集中刪除所有項目勃教,將其重置為空狀態(tài)淤击,以便進行下一批更新。
[insertedSectionSet removeAllObjects];
}
你可以看到故源,當我們準備更新時污抬,我們的布局會檢查更新動作,看看是否是一個正在插入的 item绳军。如果是印机,它就會向集合中添加一個 NSNumber
實例,代表 item 在 section 中的索引门驾。在集合(NSSet)中射赛,重復的 item 會被忽略,所以我們不必檢查它是否已經(jīng)存在奶是。
當更新完成后楣责,我們從可變集中刪除所有項目,將其重置為空狀態(tài)聂沙,以便進行下一批更新秆麸。
現(xiàn)在,我們已經(jīng)完成了這些工作及汉,讓我們來看看在 item 和裝飾視圖中的動畫代碼蛔屹,如清單4.12所示。
// 自定義動畫豁生,添加裝飾視圖
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingDecorationElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)decorationIndexPath {
// 返回 nil 則執(zhí)行默認的 crossfade 動畫
UICollectionViewLayoutAttributes *layoutAttributes;
if ([elementKind isEqualToString:AFCollectionViewFlowLayoutBackgroundDecoration]) {
if ([insertedSectionSet containsObject:@(decorationIndexPath.section)]) {
layoutAttributes = [self layoutAttributesForDecorationViewOfKind:elementKind atIndexPath:decorationIndexPath];
layoutAttributes.alpha = 0.0f;
layoutAttributes.transform3D = CATransform3DMakeTranslation(-CGRectGetWidth(layoutAttributes.frame), 0, 0);
}
}
return layoutAttributes;
}
// 自定義動畫,添加 item
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
// 返回 nil 則執(zhí)行默認的 crossfade 動畫
UICollectionViewLayoutAttributes *layoutAttributes;
if ([insertedSectionSet containsObject:@(itemIndexPath.section)]) {
layoutAttributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
layoutAttributes.transform3D = CATransform3DMakeTranslation([self collectionViewContentSize].width, 0, 0);
}
return layoutAttributes;
}
因為默認的實現(xiàn)返回 nil
漫贞,所以我們不必擔心調(diào)用 super
關(guān)鍵字以調(diào)用父類實現(xiàn)甸箱。
這兩個實現(xiàn)是相似的,因為它們構(gòu)造的動畫非常相似迅脐。對于裝飾視圖芍殖,我們檢查確保裝飾視圖是我們設(shè)置的那個;雖然沒有其他裝飾視圖谴蔑,但這是很好的實踐豌骏,以防我們以后添加更多的裝飾視圖。
無論在哪種情況下隐锭,我們都要檢查確保索引路徑的 item 部分是否包含在我們插入的部分集合中窃躲。如果是,我們從我們之前實現(xiàn)的 layoutAttributesForItemAtIndexPath:
或layoutAttributesForDecorationViewOfKind:atIndexPath:
中抓取一個UICollectionViewLayoutAttributes
的實例--我們在利用我們已經(jīng)寫好的代碼钦睡。
然后蒂窒,我們設(shè)置一個變換,將裝飾視圖向左移動,將單元格向右移動洒琢,使它們在動畫開始時完全脫離可見的集合視圖秧秉。我們還將裝飾視圖的alpha
設(shè)置為零,這樣它就會漸漸消失衰抑。
現(xiàn)在象迎,每當插入一個新的部分,用戶就會看到文件夾從左邊移入呛踊,而照片從右邊移入砾淌。這是一個非常好的觸動。
從這一節(jié)中恋技,你應(yīng)該有一個關(guān)鍵的架構(gòu)啟示拇舀,那就是編寫 UICollectionViewFlowLayout
子類就是盡可能地依賴現(xiàn)有的代碼。如果你發(fā)現(xiàn)自己要做復雜的數(shù)學計算一些已經(jīng)布局好的東西蜻底,請檢查是否有一些方法可以訪問這些信息骄崩。
使用自定義屬性布局 item
UICollectionViewLayoutAttributes
是一個類,這意味著我們可以對它進行子類化薄辅。為什么我們要這么做呢要拂?當然是為了增加對更多屬性的支持! 讓我們來看看我的意思。
該類包含以下屬性站楚,它們在運行時應(yīng)用于項目:
- Frame (convenience property for center and size)
- Center
- Size
- 3D Transform
- Alpha (opacity)
- Z-index
- Hidden
- Element category (cell, supplementary view, or decoration view)
- Element kind (nil for cells)
以上這些屬性非常棒脱惰,你可以用它們來完成很多事情。但是有時候窿春,你可能想添加自己的屬性拉一。
這就是我們現(xiàn)在要做的。
這個項目在示例代碼中叫做 Dimensions旧乞。它已經(jīng)完成了一些圖像和模型的設(shè)置蔚润,我在這里不做介紹。它要解決的問題是尺栖,照片有時在拉伸到縱橫向填充時看起來是最好的嫡纠,裁剪圖像中多余的部分以適合它的容器。其他時候延赌,你想使用縱橫適配除盏,它將縮小圖像,使整個圖像在一個容器中可見挫以。我們將編寫一個布局者蠕,作為布局屬性來處理這個問題。
我用 Single View application 模板創(chuàng)建了一個新的 Xcode 項目掐松。在刪除了.xib之后蠢棱,我將應(yīng)用程序委托中的主窗口設(shè)置改為清單 4.13 的樣子锌杀。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:[[AFViewController alloc] init]];
navigationController.navigationBar.barStyle = UIBarStyleBlack;
self.viewController = navigationController;
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
我們所做的就是用 Xcode 為我們創(chuàng)建的根自定義視圖控制器設(shè)置一個導航控制器,我們稍后將實現(xiàn)這個控制器泻仙。注意糕再,我必須將 viewController
屬性的類型改為通用的 UIViewController
。
現(xiàn)在我們已經(jīng)在屏幕上有了我們的視圖控制器玉转,我們可以設(shè)置集合視圖和布局了(見清單4.14)突想。
@implementation AFViewController
{
// 數(shù)組模型對象
NSArray *photoModelArray;
UISegmentedControl *aspectChangeSegmentedControl;
AFCollectionViewFlowLayout *photoCollectionViewLayout;
}
-(void)loadView {
// 創(chuàng)建自定義布局對象實例
photoCollectionViewLayout = [[AFCollectionViewFlowLayout alloc] init];
// 創(chuàng)建自定義集合視圖
UICollectionView *photoCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:photoCollectionViewLayout];
photoCollectionView.dataSource = self;
photoCollectionView.delegate = self;
// 注冊重用 cell
[photoCollectionView registerClass:[AFCollectionViewCell class] forCellWithReuseIdentifier:CellIdentifier];
// Set up the collection view geometry to cover the whole screen in any orientation and other view properties.
photoCollectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
photoCollectionView.allowsSelection = NO; // 禁用選擇
photoCollectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
// 添加自定義集合視圖
self.collectionView = photoCollectionView;
// 初始化模型
[self setupModel];
}
這應(yīng)該是你熟悉的代碼了。注意究抓,我們禁用了集合視圖中所有單元格的選擇交互猾担。我們還有一個分段控件作為實例變量。這個控件將放在導航欄中刺下,這樣用戶就可以在縱橫交錯和縱橫填充之間進行選擇绑嘹。
我們稍后將實現(xiàn) loadView
中引用的 AFCollectionViewFlowLayout
類,但我們先看看視圖控制器的其他代碼橘茉。它在我們的導航欄中設(shè)置了分段控件(見清單4.15)工腋。
-(void)viewDidLoad {
[super viewDidLoad];
// 在導航欄上添加自定義 UISegmentedControl 對象
aspectChangeSegmentedControl = [[UISegmentedControl alloc] initWithItems:@[@"Aspect Fit", @"Aspect Fill"]];
aspectChangeSegmentedControl.selectedSegmentIndex = 0;
[aspectChangeSegmentedControl addTarget:self action:@selector(aspectChangeSegmentedControlDidChangeValue:) forControlEvents:UIControlEventValueChanged];
self.navigationItem.titleView = aspectChangeSegmentedControl;
}
視圖控制器的其余實現(xiàn)是非常標準的(見清單4.16)。
//A handy method to implement — returns the photo model at any index path
-(AFPhotoModel *)photoModelForIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.item >= [photoModelArray count]) return nil;
return photoModelArray[indexPath.item];
}
//Configures a cell for a given index path
-(void)configureCell:(AFCollectionViewCell *)cell forIndexPath:(NSIndexPath *)indexPath
{
// Set the image for the cell
[cell setImage:[[self photoModelForIndexPath:indexPath] image]];
}
#pragma mark - UICollectionViewDataSource
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
//Return the number of photos in our model array
return [photoModelArray count];
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
AFCollectionViewCell *cell = (AFCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
// 配置 cell
[self configureCell:cell forIndexPath:indexPath];
return cell;
}
在我們的視圖控制器中剩下的最后一個方法將是響應(yīng)用戶與分段控件交互的方法(見清單4.17)畅卓。
-(void)aspectChangeSegmentedControlDidChangeValue:(id)sender
{
// We need to explicitly tell the collection view layout that we want the change animated.
[UIView animateWithDuration:0.5f animations:^{
// 在兩種布局方式之間進行切換
if (self->photoCollectionViewLayout.layoutMode == AFCollectionViewFlowLayoutModeAspectFill) {
self->photoCollectionViewLayout.layoutMode = AFCollectionViewFlowLayoutModeAspectFit;
} else {
self->photoCollectionViewLayout.layoutMode = AFCollectionViewFlowLayoutModeAspectFill;
}
}];
}
我們還沒有定義 layoutMode
屬性擅腰,所以我們現(xiàn)在就去做。這就是自定義布局屬性子類的作用翁潘。我們要添加一個新的布局屬性來指定照片的縮放模式趁冈。創(chuàng)建一個新的類,它是 UICollectionViewLayoutAttributes
的子類(見清單4.18)拜马。
typedef enum : NSUInteger{
AFCollectionViewFlowLayoutModeAspectFit, //Default
AFCollectionViewFlowLayoutModeAspectFill
}AFCollectionViewFlowLayoutMode;
@interface AFCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes
@property (nonatomic, assign) AFCollectionViewFlowLayoutMode layoutMode;
@end
這就是我們真正需要的所有東西--布局模式的定義和一個持有它們的屬性渗勘。然而,看看 UICollectionViewLayoutAttributes
的定義俩莽;注意它遵守 NSCopying
協(xié)議呀邢。非常重要的是,我們也要遵守這個協(xié)議并實現(xiàn)copyWithZone
方法豹绪。(見清單4.19)。否則申眼,我們的屬性將始終為零(編譯器保證的)瞒津。在iOS 7中的新功能。你現(xiàn)在必須在子類化布局屬性時覆蓋 isEqual:
方法巷蚪。
#import "AFCollectionViewLayoutAttributes.h"
@implementation AFCollectionViewLayoutAttributes
-(id)copyWithZone:(NSZone *)zone {
AFCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
attributes.layoutMode = self.layoutMode;
return attributes;
}
@end
現(xiàn)在我們可以實現(xiàn)我們的流式布局子類了。我創(chuàng)建了一個名為AFCollectionViewFlowLayout
的新類濒翻,它是UICollectionViewFlowLayout
的子類屁柏。它如清單 4.20 所示啦膜,從本章前面展示的改進的 Survey 應(yīng)用中應(yīng)該看起來很熟悉。
#import <UIKit/UIKit.h>
#import "AFCollectionViewLayoutAttributes.h"
#define kMaxItemDimension 100
#define kMaxItemSize CGSizeMake(kMaxItemDimension, kMaxItemDimension)
@protocol AFCollectionViewDelegateFlowLayout <UICollectionViewDelegateFlowLayout>
@optional
-(AFCollectionViewFlowLayoutMode)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout layoutModeForItemAtIndexPath:(NSIndexPath *)indexPath;
@end
@interface AFCollectionViewFlowLayout : UICollectionViewFlowLayout
@property (nonatomic, assign) AFCollectionViewFlowLayoutMode layoutMode;
@end
我們所做的是擴展 UICollectionViewDelegateFlowLayout
協(xié)議來創(chuàng)建我們自己的布局淌喻。就像我們?yōu)?Survey
應(yīng)用定制單個單元格的大小一樣僧家,我們希望提供一個接口,讓使用我們布局的開發(fā)人員可以為他們單元格中的照片指定單獨的縱橫比裸删。
現(xiàn)在我們已經(jīng)有了我們的自定義布局屬性類八拱,讓我們簡單地看看我們的自定義布局的部分,你應(yīng)該已經(jīng)熟悉了(見清單4.21)涯塔。
-(id)init {
if (!(self = [super init])) return nil;
// Some basic setup. 140x140 + 3*13 ~= 320, so we can get a two-column grid in portrait orientation.
self.itemSize = kMaxItemSize;
self.sectionInset = UIEdgeInsetsMake(13.0f, 13.0f, 13.0f, 13.0f);
self.minimumInteritemSpacing = 13.0f;
self.minimumLineSpacing = 13.0f;
return self;
}
-(void)applyLayoutAttributes:(AFCollectionViewLayoutAttributes *)attributes {
// Check for representedElementKind being nil, indicating this is a cell and not a header or decoration view
if (attributes.representedElementKind == nil)
{
// Pass our layout mode onto the layout attributes
attributes.layoutMode = self.layoutMode;
if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:layoutModeForItemAtIndexPath:)])
{
attributes.layoutMode = [(id<AFCollectionViewDelegateFlowLayout>)self.collectionView.delegate collectionView:self.collectionView layout:self layoutModeForItemAtIndexPath:attributes.indexPath];
}
}
}
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];
for (AFCollectionViewLayoutAttributes *attributes in attributesArray)
{
[self applyLayoutAttributes:attributes];
}
return attributesArray;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
AFCollectionViewLayoutAttributes *attributes = (AFCollectionViewLayoutAttributes *)[super layoutAttributesForItemAtIndexPath:indexPath];
[self applyLayoutAttributes:attributes];
return attributes;
}
這與我們在本章前面的第一個流式布局子類中看到的代碼是一樣的肌稻,不同的是我們使用的是 AFCollectionViewLayoutAttributes
而不是 UICollectionViewLayoutAttributes
,而且我們還傳遞了我們的 layoutMode
匕荸。
在 applyLayoutAttributes:
中爹谭,我們檢查集合視圖的委托,看它是否響應(yīng)我們在AFCollectionViewDelegateFlowLayout
協(xié)議中定義的選擇器榛搔。如果它響應(yīng)了诺凡,我們就把它投給一個符合協(xié)議的 id,這樣我們就可以從它那里抓取布局模式药薯。
觀察敏銳的讀者可能會問自己绑洛,集合視圖是如何知道使用我們自定義的UICollectionViewLayoutAttributes
子類的。答案很簡單童本。我們的布局需要實現(xiàn)一個類方法來告訴集合視圖使用哪個自定義類(見清單4.22)真屯。顯然,默認的實現(xiàn)會返回UICollectionViewLayoutAttributes
穷娱。
+(Class)layoutAttributesClass
{
// Important for letting UICollectionView know what kind of attributes to use.
return [AFCollectionViewLayoutAttributes class];
}
唯一缺少的另一個組件是我們的布局可能最終處于無效狀態(tài)绑蔫。如果我們改變布局模式而不更新已經(jīng)在屏幕上布局的單元格,已經(jīng)顯示的單元格將仍然應(yīng)用舊的布局泵额,而由于滾動或插入而變得可見的單元格將擁有新的布局配深。我們需要的是,每當我們的布局模式發(fā)生變化時嫁盲,就調(diào)用 invalidateLayout
方法篓叶。
-(void)setLayoutMode:(AFCollectionViewFlowLayoutMode)layoutMode {
// Update our backing ivar...
_layoutMode = layoutMode;
// 然后使我們舊的布局無效。
[self invalidateLayout];
}
我知道我們已經(jīng)寫了很多代碼羞秤,但沒有任何回報缸托,但請再忍耐一下。即使我們有了我們的自定義布局瘾蛋,并且正在設(shè)置自定義屬性俐镐,我們?nèi)匀粵]有任何代碼將該屬性應(yīng)用到單元格中。我創(chuàng)建了一個 UICollectionViewCell
的子類 AFCollectionViewCell
哺哼。它顯示由其setImage:
方法設(shè)置的圖像佩抹。清單 4.24 所示的實現(xiàn)叼风,與第 3 章的 Survey 應(yīng)用程序中使用的實現(xiàn)幾乎相同。然而棍苹,存在兩個關(guān)鍵的區(qū)別无宿。
首先,我們?yōu)椴季帜J铰暶髁艘粋€實例變量廊勃,其次训堆,我們在一個新的方法中使用該實例變量來設(shè)置圖像視圖的 frame
蜻韭。這個問題與 iOS 7 中引擎的變化有關(guān)拜银;現(xiàn)在方法的調(diào)用順序不同缔逛,所以每當設(shè)置一個新的圖像時,設(shè)置圖像的 frame
是很重要的(這很有意義冰悠,因為圖像的 frame
取決于圖像的縱橫比堡妒,而我們在設(shè)置 UIImage
實例之前是不知道的)。
@implementation AFCollectionViewCell
{
UIImageView *imageView;
AFCollectionViewFlowLayoutMode layoutMode;
}
-(void)prepareForReuse {
[super prepareForReuse];
[self setImage:nil];
}
- (id)initWithFrame:(CGRect)frame {
if (!(self = [super initWithFrame:frame])) return nil;
// Set up our image view
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame))];
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
imageView.clipsToBounds = YES;
[self.contentView addSubview:imageView];
// This will make the rest of our cell, outside the image view, appear transparent against a black background.
self.backgroundColor = [UIColor blackColor];
return self;
}
#pragma mark - Public Methods
-(void)setImage:(UIImage *)image {
[imageView setImage:image];
[self setImageViewFrame];
}
- (void)setImageViewFrame {
CGSize imageViewSize = self.bounds.size;
if (layoutMode == AFCollectionViewFlowLayoutModeAspectFit) {
CGSize photoSize = imageView.image.size;
CGFloat aspectRatio = photoSize.width / photoSize.height;
if (aspectRatio < 1) {
imageViewSize = CGSizeMake(CGRectGetWidth(self.bounds) * aspectRatio, CGRectGetHeight(self.bounds));
} else {
imageViewSize = CGSizeMake(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds) / aspectRatio);
}
// 設(shè)置 imageView 的尺寸
imageView.bounds = CGRectMake(0, 0, imageViewSize.width, imageViewSize.height);
// 設(shè)置 imageView 的中心點
imageView.center = CGPointMake(CGRectGetMinX(self.bounds), CGRectGetMidY(self.bounds));
}
}
@end
重要的是溉卓,圖像視圖的 clipsToBounds
屬性被設(shè)置為 YES
皮迟。這就確保了當照片被縮放以適合于圖像視圖,并裁剪自身的一部分時桑寨,被裁剪的區(qū)域?qū)⒉豢梢姟?/p>
接下來伏尼,我們有代碼來實際應(yīng)用布局模式到單元格中(見清單4.25)。
-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
[super applyLayoutAttributes:layoutAttributes];
// Important! Check to make sure we're actually this special subclass.
// Failing to do so could cause the app to crash!
if (![layoutAttributes isKindOfClass:[AFCollectionViewLayoutAttributes class]]) {
return;
}
AFCollectionViewLayoutAttributes *castedLayoutAttributes = (AFCollectionViewLayoutAttributes *)layoutAttributes;
//start out with the detail image size of the maximum size
CGSize imageViewSize = self.bounds.size;
if (castedLayoutAttributes.layoutMode == AFCollectionViewFlowLayoutModeAspectFit) {
//Determine the size and aspect ratio for the model's image
CGSize photoSize = imageView.image.size;
CGFloat aspectRatio = photoSize.width / photoSize.height;
if (aspectRatio < 1) {
//The photo is taller than it is wide, so constrain the width
imageViewSize = CGSizeMake(CGRectGetWidth(self.bounds) * aspectRatio, CGRectGetHeight(self.bounds));
} else if (aspectRatio > 1) {
//The photo is wider than it is tall, so constrain the height
imageViewSize = CGSizeMake(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds) / aspectRatio);
}
}
// 調(diào)整 imageView 的尺寸尉尾、位置
// Set the size of the imageView ...
imageView.bounds = CGRectMake(0, 0, imageViewSize.width, imageViewSize.height);
// And the center, too.
imageView.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
}
這個方法屬于 UICollectionReusableView
爆阶,因為布局屬性適用于單元格 item、輔助視圖和裝飾視圖沙咏。首先辨图,你必須調(diào)用 super
覆蓋父類的實現(xiàn)。接下來肢藐,它檢查確保布局屬性是我們自定義子類的一個實例故河,然后再投遞指針。
我們使用布局模式來決定是否應(yīng)該將圖像視圖的大小設(shè)置為我們的 bounds
大小吆豹,或者是否應(yīng)該調(diào)整它鱼的。如果模式是縱橫適配,我們使用類似于第3章 "內(nèi)容上下文化 "中的調(diào)查視圖控制器的邏輯來調(diào)整它痘煤。最后凑阶,我們設(shè)置圖像視圖的邊界和中心。我們使用大小和位置而不是 contentMode
速勇,這樣我們就可以很容易地從一種模式過渡到另一種模式的動畫。bounds
和 center
是隱式的可動畫屬性)坎拐。
最后烦磁,在所有這些代碼之后养匈,你可以運行應(yīng)用程序,并在縱橫向適合和縱橫向填充照片之間進行過渡(見圖 4.6)都伪。它將對過渡進行動畫處理呕乎,即使是滾動或旋轉(zhuǎn)的動畫。
Aspect Fit | Aspect Fill |
---|---|
...
網(wǎng)格視圖之外
到目前為止陨晶,我們看到的流式布局所做的都是網(wǎng)格視圖的一些變化猬仁。雖然網(wǎng)格布局的確是一種基于線條的、打破常規(guī)的布局方式先誉,但它只是這種布局的一種特殊情況湿刽。讓我們更進一步,做一些真正有趣的事情褐耳。
我們要實現(xiàn)一個封面流布局诈闺。在此之前,我要特別感謝 Mark Pospesel 在 GitHub 上建立了他的 Introducing Collection Views 項目铃芦。我書中這一節(jié)的代碼大量借鑒了他的例子雅镊,經(jīng)他許可使用。本節(jié)的示例代碼可以以 Cover Flow 的名義獲得刃滓。
創(chuàng)建一個標準的 “Single-View Xcode 項目并刪除.xib文件" 之后仁烹,我們要做的第一件事是在項目導航器窗格中打開項目設(shè)置。在 "Build Phases"中咧虎,展開 "Link Binary with Libraries "并點擊加號卓缰。選擇并添加 QuartzCore 框架,打開 Supporting Files 組下的 Prefix 文件老客。我的叫 CoverFlowPrefix.pch
僚饭;它是一個頭文件,會被導入到所有的頭文件中胧砰。添加 #import <QuartzCore/QuartzCore.h>
到 PCH 中△⑼遥現(xiàn)在我們可以在整個項目中訪問所有的 QuartzCore 框架。我們以后會需要這個來使用 CALayer
尉间。這一步對我來說是創(chuàng)建 Xcode 項目中很常見的一步偿乖,蘋果公司默認不包含它真是個奇跡。
視圖控制器將與 Dimensions 非常相似哲嘲,只是這次我們將有兩個布局贪薪。我們將像上次一樣,在導航欄中使用一個分段控件來切換這兩種布局(見清單4.26)眠副。
@implementation AFViewController
{
// Array of selection objects
NSArray *photoModelArray;
UISegmentedControl *layoutChangeSegmentedControl;
AFCoverFlowFlowLayout *coverFlowCollectionViewLayout;
UICollectionViewFlowLayout *boringCollectionViewLayout;
}
// Static identifiers for cells and supplementary views
static NSString *CellIdentifier = @"CellIdentifier";
-(void)loadView
{
// Create our view
/**
MARK:這里創(chuàng)建了兩個布局對象画切,一個是自定義的 AFCoverFlowFlowLayout,另一個是 UICollectionViewFlowLayout囱怕。
通過 UISegmentedControl 進行布局方式的切換
*/
// 初始化自定義集合視圖布局霍弹,封面流布局
coverFlowCollectionViewLayout = [[AFCoverFlowFlowLayout alloc] init];
// 創(chuàng)建一個基本的流程布局毫别,將在縱向容納三列
boringCollectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
boringCollectionViewLayout.itemSize = CGSizeMake(140, 140);
boringCollectionViewLayout.minimumLineSpacing = 10.0f;
boringCollectionViewLayout.minimumInteritemSpacing = 10.0f;
// Create a new collection view with our flow layout and set ourself as delegate and data source
UICollectionView *photoCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:boringCollectionViewLayout];
photoCollectionView.dataSource = self;
photoCollectionView.delegate = self;
// Register our classes so we can use our custom subclassed cell and header
[photoCollectionView registerClass:[AFCollectionViewCell class] forCellWithReuseIdentifier:CellIdentifier];
// Set up the collection view geometry to cover the whole screen in any orientation and other view properties
photoCollectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
photoCollectionView.allowsSelection = NO;
photoCollectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
// Finally, set our collectionView (since we are a collection view controller, this also sets self.view)
self.collectionView = photoCollectionView;
// Set up our model
[self setupModel];
}
-(void)viewDidLoad {
[super viewDidLoad];
// Crate a segmented control to sit in our navigation bar
layoutChangeSegmentedControl = [[UISegmentedControl alloc] initWithItems:@[@"Boring", @"Cover Flow"]];
layoutChangeSegmentedControl.selectedSegmentIndex = 0;
[layoutChangeSegmentedControl addTarget:self action:@selector(layoutChangeSegmentedControlDidChangeValue:) forControlEvents:UIControlEventValueChanged];
self.navigationItem.titleView = layoutChangeSegmentedControl;
}
配置集合視圖的數(shù)據(jù)源方法與上一節(jié)中使用的方法完全相同,因此這里沒有顯示它們典格。然而岛宦,我們將實現(xiàn)一個新的UICollectionViewDelegateFlowLayout
方法,它將負責返回我們布局的邊緣插入量(見清單4.27)耍缴。我們使用這種方法是因為 Cover Flow 布局需要不同的 section 邊緣插入量砾肺,這取決于接口的方向和它運行的具體設(shè)備。如果可能的話防嗡,我喜歡把這種邏輯保留在UICollectionViewLayout
子類之外变汪。
// 自定義 section 邊緣插入量
-(UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
if (collectionViewLayout == boringCollectionViewLayout) {
// A basic flow layout that will accommodate three columns in portrait
return UIEdgeInsetsMake(10, 20, 10, 20);
} else {
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
// Portrait is the same in either orientation
return UIEdgeInsetsMake(0, 70, 0, 70);
} else {
// We need to get the height of the main screen to see if we're running
// on a 4" screen. If so, we need extra side padding.
if (CGRectGetHeight([[UIScreen mainScreen] bounds]) > 480) {
return UIEdgeInsetsMake(0, 190, 0, 190);
} else {
return UIEdgeInsetsMake(0, 150, 0, 150);
}
}
}
}
這些值主要是通過實驗來確定的,看看什么看起來是正確的本鸣。我鼓勵你采取這種方法疫衩,而不是用數(shù)學方法來預(yù)測它們,原因很簡單荣德,如果你的用戶看起來不正確闷煤,那么某件事在數(shù)學上是否正確并不重要。
最后涮瞻,我們需要實現(xiàn)我們的用戶交互代碼鲤拿。如清單4.28所示,你會發(fā)現(xiàn)它與上一個例子類似署咽。
// !!!: 動態(tài)更新集合視圖布局
- (void)layoutChangeSegmentedControlDidChangeValue:(id)sender {
// Change to the alternate layout
if (layoutChangeSegmentedControl.selectedSegmentIndex == 0) {
[self.collectionView setCollectionViewLayout:boringCollectionViewLayout animated:NO];
} else {
[self.collectionView setCollectionViewLayout:coverFlowCollectionViewLayout animated:NO];
}
// Invalidate the new layout
[self.collectionView.collectionViewLayout invalidateLayout];
}
我們明確地不對布局的變化進行動畫處理近顷,因為它們之間的差別太大,它們之間的動畫對用戶來說顯得很刺眼宁否。正如你在下一章中看到的那樣窒升,用動畫在布局之間進行改變其實是很容易做到的。
在改變布局之后慕匠,我們需要將新布局無效化饱须。雖然這一點沒有包含在文檔中,但我注意到台谊,如果你省略了這一點蓉媳,一些布局會出現(xiàn)一些奇怪的行為。實驗一下锅铅,看看什么對你的自定義布局有效酪呻。
我們將創(chuàng)建一個新的自定義 UICollectionViewLayoutAttributes
子類,以保存兩個值:一個用于指示我們是否應(yīng)該柵格化圖層盐须,另一個用于指示單元格應(yīng)該如何 "遮擋"玩荠。我們不能使用 alpha
,因為半透明的后面的單元格會 "滲入"。新的子類如清單4.29所示阶冈。對于我們的封面視圖布局屉凯,單元格將始終是柵格化的,因為否則它們會因為3D變換而得到一些鋸齒狀的邊緣眼溶。
至于遮罩層,我們希望不在集合視圖中心的項目不要那么突出晓勇,所以我們會在每個單元格的頂部放置一個半透明的遮罩視圖堂飞。
// AFCollectionViewLayoutAttributes.h
@interface AFCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes
@property (nonatomic, assign) BOOL shouldRasterize;
@property (nonatomic, assign) CGFloat maskingValue;
@end
// AFCollectionViewLayoutAttributes.m
#import "AFCollectionViewLayoutAttributes.h"
@implementation AFCollectionViewLayoutAttributes
-(id)copyWithZone:(NSZone *)zone {
AFCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
attributes.shouldRasterize = self.shouldRasterize;
attributes.maskingValue = self.maskingValue;
return attributes;
}
@end
開啟
shouldRasterize
后,CALayer
會被光柵化為 bitmap绑咱,layer
的陰影等效果也會被保存到 bitmap 中绰筛。
接下來,讓我們看看自定義的 UICollectionViewFlowLayout
子類本身(見清單4.30)描融。我省略了文件頂部的#定義铝噩,這些定義在后面會用到。我將把它們包含在那里窿克。
@implementation AFCoverFlowFlowLayout
#pragma mark - Overridden Methods
-(instancetype)init {
if (!(self = [super init])) return nil;
// Set up our basic properties
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.itemSize = CGSizeMake(180, 180);
self.minimumLineSpacing = -60; // Gets items up close to one another
self.minimumInteritemSpacing = 200; // Makes sure we only have 1 row of items in portrait mode
return self;
}
// !!!: 返回自定義布局屬性對象
+(Class)layoutAttributesClass {
return [AFCollectionViewLayoutAttributes class];
}
/**
!!!: 當用戶滾動集合視圖時骏庸,單元格的變換(在每一幀刷新時)都會被重新計算
*/
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)oldBounds {
// Very important — needed to re-layout the cells when scrolling.
return YES;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray* layoutAttributesArray = [super layoutAttributesForElementsInRect:rect];
// We're going to calculate the rect of the collection view visible to the user.
CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x, self.collectionView.contentOffset.y, CGRectGetWidth(self.collectionView.bounds), CGRectGetHeight(self.collectionView.bounds));
for (UICollectionViewLayoutAttributes* attributes in layoutAttributesArray)
{
// We're going to calculate the rect of the collection view visible to the user.
// That way, we can avoid laying out cells that are not visible.
if (CGRectIntersectsRect(attributes.frame, rect))
{
[self applyLayoutAttributes:attributes forVisibleRect:visibleRect];
}
}
return layoutAttributesArray;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForItemAtIndexPath:indexPath];
// We're going to calculate the rect of the collection view visible to the user.
CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x, self.collectionView.contentOffset.y, CGRectGetWidth(self.collectionView.bounds), CGRectGetHeight(self.collectionView.bounds));
[self applyLayoutAttributes:attributes forVisibleRect:visibleRect];
return attributes;
}
其中大部分是標準的流程布局代碼。然而年叮,請注意具被,我們正在計算集合視圖中的可見矩形。這個矩形將被用來決定對每個單元格應(yīng)用多少3D變換和轉(zhuǎn)換只损。我們將通過獲取集合視圖的內(nèi)容偏移和邊界大小來輕松計算一姿。
我們還在 shouldInvalidateLayoutForBoundsChange
中返回 YES
,這樣當用戶滾動集合視圖時跃惫,單元格的變換會被重新計算(在每一幀刷新時)叮叹。
最小線間距(minimumLineSpacing
)為負值,因為我們希望我們的單元格 "捆綁 "在一起爆存,而在水平滾動的集合視圖中蛉顽,線間距是每個垂直列單元格之間的距離。如圖 4.7 所示终蒂,行間距是按行間的空間計算的蜂林,而項目間的間距是沿行的單元格之間的空間。
這可能是一個棘手的問題拇泣,所以請記住噪叙,在垂直滾動的集合視圖中,行間距和項目間的間距分別類似于寫作中的行高和內(nèi)核霉翔。在水平滾動的集合視圖中睁蕾,它們是翻轉(zhuǎn)的。
接下來是用于對我們的單元格應(yīng)用透視三維變換的密集數(shù)學計算(見清單4.31)。再次子眶,我需要感謝 Mark Pospesel 的幫助)瀑凝。
#define ACTIVE_DISTANCE 100
#define TRANSLATE_DISTANCE 100
#define ZOOM_FACTOR 0.2f
#define FLOW_OFFSET 40
#define INACTIVE_GREY_VALUE 0.6f
#pragma mark - Private Custom Methods
/**
!!!: 自定義布局,通過 item 與中心點的距離執(zhí)行 3D 變換
*/
-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes forVisibleRect:(CGRect)visibleRect
{
// Applies the cover flow effect to the given layout attributes.
// We want to skip supplementary views.
if (attributes.representedElementKind) return;
// Calculate the distance from the center of the visible rect to the center of the attributes.
// Then normalize it so we can compare them all. This way, all items further away than the
// active get the same transform.
CGFloat distanceFromVisibleRectToItem = CGRectGetMidX(visibleRect) - attributes.center.x;
CGFloat normalizedDistance = distanceFromVisibleRectToItem / ACTIVE_DISTANCE;
BOOL isLeft = distanceFromVisibleRectToItem > 0;
CATransform3D transform = CATransform3DIdentity;
CGFloat maskAlpha = 0.0f;
if (fabs(distanceFromVisibleRectToItem) < ACTIVE_DISTANCE) {
// We're close enough to apply the transform in relation to
// how far away from the center we are.
transform = CATransform3DTranslate(CATransform3DIdentity, (isLeft? - FLOW_OFFSET : FLOW_OFFSET)*ABS(distanceFromVisibleRectToItem/TRANSLATE_DISTANCE), 0, (1 - fabs(normalizedDistance)) * 40000 + (isLeft? 200 : 0));
// Set the perspective of the transform.
transform.m34 = -1/(4.6777 * self.itemSize.width);
// Set the zoom factor.
CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));
transform = CATransform3DRotate(transform, (isLeft? 1 : -1) * fabs(normalizedDistance) * 45 * M_PI / 180, 0, 1, 0);
transform = CATransform3DScale(transform, zoom, zoom, 1);
attributes.zIndex = 1;
CGFloat ratioToCenter = (ACTIVE_DISTANCE - fabs(distanceFromVisibleRectToItem)) / ACTIVE_DISTANCE;
// Interpolate between 0.0f and INACTIVE_GREY_VALUE
maskAlpha = INACTIVE_GREY_VALUE + ratioToCenter * (-INACTIVE_GREY_VALUE);
} else {
// We're too far away - just apply a standard perspective transform.
transform.m34 = -1/(4.6777 * self.itemSize.width);
transform = CATransform3DTranslate(transform, isLeft? -FLOW_OFFSET : FLOW_OFFSET, 0, 0);
transform = CATransform3DRotate(transform, (isLeft? 1 : -1) * 45 * M_PI / 180, 0, 1, 0);
attributes.zIndex = 0;
maskAlpha = INACTIVE_GREY_VALUE;
}
attributes.transform3D = transform;
// Rasterize the cells for smoother edges.
[(AFCollectionViewLayoutAttributes *)attributes setShouldRasterize:YES];
[(AFCollectionViewLayoutAttributes *)attributes setMaskingValue:maskAlpha];
}
喑艚堋粤咪!不要擔心,如果它看起來像很多渴杆。我將介紹更多細節(jié)寥枝,你可以稍后再做具體實驗,畢竟這不是一本關(guān)于 CATransform3D 的書磁奖。最重要的是囊拜,你可以通過集合視圖在三維空間應(yīng)用變換”却睿酷斃了!
如果屬性的 itme 足夠接近可見區(qū)域的中心冠跷,第一個if分支就會執(zhí)行。它將根據(jù)它與中心的接近程度身诺,給它進行縮放蜜托、平移和三維透視變換。如果一個 item 正好在中心霉赡,那么變換就不會有任何作用盗冷。
如果 item 離中心足夠遠,則 else 分支會執(zhí)行同廉,以確保 item 不會變得過于變換仪糖。想象一下,在 Cover Flow 中迫肖,延伸到邊緣的 item 不斷地被應(yīng)用了越來越多的變換锅劝,它們最終會變得如此變換,以至于它們會翻轉(zhuǎn)到它們的另一邊蟆湖!我們還想設(shè)置一個默認的變換分支故爵。
我們還要設(shè)置默認的蒙版值為0,并始終將光柵化設(shè)置為YES∮缃颍現(xiàn)在讓我們運行應(yīng)用程序诬垂,看看發(fā)生了什么。請注意伦仍,你可以非常容易地在普通流布局和封面流布局之間切換(見圖4.8)结窘。
Boring Flow | Corver Flow |
---|---|
它看起來很棒。然而充蓝,這有幾個問題隧枫。首先喉磁,注意到集合視圖在單元格之間停了一半;在真正的 Cover Flow 中官脓,滾動視圖在一個項目完全居中的情況下停止协怒。其次,你可以清楚地看到卑笨,我們的蒙版和柵格化的布局屬性沒有被應(yīng)用孕暇。嗯,那是因為我們還沒有應(yīng)用遮罩和光柵化的布局屬性赤兴。哦芭商,那是因為我們還沒有寫出這樣的代碼。我們先來處理第一個問題搀缠。
targetContentOffsetForProposedContentOffset:withScrollingVelocity:
是一個定義在 UICollectionViewLayout
中的方法,并且可以被重寫近迁。
子類艺普,包括我們的子類。它為子類提供了一個定義集合視圖將 "扣 "到哪里的機會鉴竭。我們要實現(xiàn)它歧譬,并使用我們在 layoutAttributesForElementsInRect:
中的現(xiàn)有代碼來獲取擬議矩形中元素的屬性(見清單4.32)。然后搏存,我們將找到其項目將最接近擬議的可見矩形中心的屬性瑰步。然后,我們將找出該項目將有多遠璧眠,并返回一個調(diào)整后的內(nèi)容偏移缩焦,使該視圖居中。
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
// 返回一個我們想要讓集合視圖停止?jié)L動的坐標點
// First, calculate the proposed center of the collection view once the collection view has stopped
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);
// Use the center to find the proposed visible rect.
CGRect proposedRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
// Get the attributes for the cells in that rect.
NSArray* array = [self layoutAttributesForElementsInRect:proposedRect];
// This loop will find the closest cell to proposed center of the collection view
for (UICollectionViewLayoutAttributes* layoutAttributes in array)
{
// We want to skip supplementary views
if (layoutAttributes.representedElementCategory != UICollectionElementCategoryCell)
continue;
// Determine if this layout attribute's cell is closer than the closest we have so far
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (fabs(itemHorizontalCenter - horizontalCenter) < fabs(offsetAdjustment)) {
offsetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
現(xiàn)在责静,我們的應(yīng)用程序?qū)⒖煺盏阶罱捻椖吭摹=酉聛碜屛覀儗崿F(xiàn)我們的UICollectionViewCell子類。清單4.33中有完整的實現(xiàn)灾螃,但重要的方法是 applyLayoutAttributes:
题翻。
@implementation AFCollectionViewCell
{
UIImageView *imageView;
UIView *maskView;
}
- (id)initWithFrame:(CGRect)frame {
if (!(self = [super initWithFrame:frame])) return nil;
// Set up our image view
imageView = [[UIImageView alloc] initWithFrame:CGRectInset(CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame)), 10, 10)];
imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
imageView.clipsToBounds = YES;
[self.contentView addSubview:imageView];
// 遮罩視圖
maskView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame))];
maskView.backgroundColor = [UIColor blackColor];
maskView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
maskView.alpha = 0.0f;
[self.contentView insertSubview:maskView aboveSubview:imageView];
// This will make the rest of our cell, outside the image view, appear transparent against a black background.
self.backgroundColor = [UIColor whiteColor];
return self;
}
#pragma mark - Overridden Methods
-(void)prepareForReuse {
[super prepareForReuse];
[self setImage:nil];
}
-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
[super applyLayoutAttributes:layoutAttributes];
maskView.alpha = 0.0f;
self.layer.shouldRasterize = NO;
// Important! Check to make sure we're actually this special subclass.
// Failing to do so could cause the app to crash!
if (![layoutAttributes isKindOfClass:[AFCollectionViewLayoutAttributes class]])
{
return;
}
AFCollectionViewLayoutAttributes *castedLayoutAttributes = (AFCollectionViewLayoutAttributes *)layoutAttributes;
self.layer.shouldRasterize = castedLayoutAttributes.shouldRasterize;
maskView.alpha = castedLayoutAttributes.maskingValue;
}
#pragma mark - Public Methods
-(void)setImage:(UIImage *)image {
[imageView setImage:image];
}
@end
現(xiàn)在我們可以運行應(yīng)用程序,看看其他單元格的 "淡入淡出 "效果和快照效果腰鬼。
很好嵌赠!玩一玩吧。實驗旋轉(zhuǎn)和改變布局熄赡,同時集合視圖正在減速姜挺。找到它的能力和局限性。
現(xiàn)在彼硫,我們已經(jīng)實現(xiàn)了想要達到的效果初家,我想談?wù)勎野l(fā)現(xiàn)的集合視圖的幾個問題。
首先,封面流視圖的旋轉(zhuǎn)動畫并不完美溜在。我似乎無法讓它實現(xiàn)無縫連接陌知;我想這可能與旋轉(zhuǎn)過程中改變 contentSize
有關(guān)。
我最初嘗試在旋轉(zhuǎn)期間將布局改為 Cover Flow掖肋,這樣在縱向時使用普通流布局仆葡,在橫向時使用 Cover Flow 布局。在旋轉(zhuǎn)過程中改變布局非常麻煩志笼,因為在旋轉(zhuǎn)過程中布局子類中的contentSize
不可靠沿盅,在改變布局時更不可靠。
我研究了這些問題纫溃,找到了布局使用時的精確事件順序腰涧。
-
prepareLayout
是在布局上調(diào)用的,這樣它就有機會進行任何前期的計算紊浩。 -
collectionViewContentSize
在布局上被調(diào)用窖铡,以確定集合視圖的內(nèi)容大小。 -
layoutAttributesForElementsInRect:
被調(diào)用坊谁。
然后费彼,布局激活,并繼續(xù)調(diào)用 layoutAttributesForElementsInRect:
和layoutAttributesForItemAtIndexPath:
口芍,直到布局變得無效箍铲。然后,再次重復這個過程鬓椭。
在布局中使用 content size 可能不是一個好主意颠猴;UICollectionView
仍然是非常新的,社區(qū)仍在確定使用它的最佳實踐小染。
根據(jù)你對布局的想法芙粱,可能最好轉(zhuǎn)向 UICollectionViewLayout
,就像我們在下一章做的那樣氧映。然而春畔,始終要先考慮 UICollectionViewFlowLayout
是否能完成你的目標。它為你做了很多繁重的工作岛都。
我們現(xiàn)在已經(jīng)涵蓋了裝飾視圖律姨、集合視圖布局、布局屬性和自定義動畫臼疫。你已經(jīng)鞏固了前三章的知識择份,并為即將到來的一章浸入了水中。我們即將做一些非常有趣的事情烫堤,但首先荣赶,讓我們回顧一下 UITableView
凤价。
UITableView:UICollectionView 它爹
UICollectionView
是在 iOS 6 中才引入的,但 UITableView
從 2008 年最初的 iPhone SDK 發(fā)布時就已經(jīng)存在了拔创。UITableView
所使用的許多相同的原則也適用于 UICollectionView
利诺,但有些已經(jīng)被修改。
UITableView
最近才開始使用類注冊方法來創(chuàng)建其單元格剩燥。這是集合視圖的唯一方法慢逾。
列表視圖的 "批量更新(batch updates)"是通過調(diào)用一個方法來開始更新,執(zhí)行更新灭红,然后調(diào)用另一個方法來表示更新結(jié)束侣滩。然而,集合視圖只提供了基于 Block 塊的 performBatchUpdates:
方法(在我看來更好)变擒。
這些都是開發(fā)人員通過類來實現(xiàn)其目標的一些微小的差異君珠。這兩個類之間更大的哲學差異是,列表視圖單元格處理了很多內(nèi)部布局娇斑。這與集合視圖單元格形成了鮮明的對比策添,集合視圖單元格完全不處理。這迫使開發(fā)者每次都要從頭開始實現(xiàn)自己的 UITableViewCell
子類悠菜。同時,UITableViewCell
有四種不同的 "樣式"败富,定義了它的兩個文本標簽悔醋、圖像視圖、"附件 "視圖和編輯樣式的布局方式兽叮。相當大的區(qū)別!
我相信芬骄,如果蘋果今天要引入 UITableView
,知道他們在過去 6 年里學到的框架設(shè)計知識鹦聪,UITableViewCell
根本不會有樣式账阻。相反,他們會有一些直接的子類泽本,開發(fā)者可以使用淘太,也可以實現(xiàn)自己的子類。
盡管以現(xiàn)代 Objective-C 框架的標準來看规丽,UITableView
顯得很臃腫蒲牧,但 UICollectionView
的圓滑在很大程度上要歸功于蘋果公司從最初制作 UITableView
時學到的經(jīng)驗。