第4章:用 UICollectionViewFlowLayout 組織內(nèi)容

注:
本文翻譯自 《iOS UICollectionView The Complete Guide 2nd Edition》
使用的翻譯工具:https://www.deepl.com/translator

你現(xiàn)在已經(jīng)掌握了使用 UICollectionView 向用戶顯示自定義內(nèi)容的技巧,可以顯示單元格以及輔助視圖。到目前為止,我們一直關(guān)注實(shí)際的內(nèi)容,而不是如何在屏幕上組織內(nèi)容。本章將探討 UICollectionView 是如何被設(shè)計(jì)成使用 UICollectionViewLayout 來(lái)組織其內(nèi)容的。我們仔細(xì)研究了 UICollectionViewFlowLayout,以及如何通過(guò)子類化它來(lái)獲得大量的可定制性,而不需要大量的額外工作。當(dāng)我們以簡(jiǎn)短的歷史課結(jié)束,我們會(huì)探索 UITableView 以及它與 UICollectionView 的關(guān)系。

什么是布局

UICollectionViewLayout 是一個(gè)抽象類,它不應(yīng)該被直接創(chuàng)建,它存在的唯一目的是被子類化。每個(gè)集合視圖都有一個(gè)與之相關(guān)聯(lián)的布局對(duì)象,它的工作是將內(nèi)容布局出來(lái)。布局對(duì)象并不關(guān)心其布局的視圖中所包含的數(shù)據(jù),它只關(guān)心向用戶展示的布局。

UICollectionViewFlow 是一個(gè)直接的子類,它以基于行的、分塊的方式來(lái)布局內(nèi)容。我們已經(jīng)看到了UICollectionViewFlowLayout 最基本的形式,一個(gè)網(wǎng)格。我們用本章的其余部分來(lái)探索一個(gè)簡(jiǎn)單的流式布局子類給你作為一個(gè)開(kāi)發(fā)者帶來(lái)的力量。如果你知道把它放在哪里的話,你可以用很少的代碼來(lái)創(chuàng)建令人震驚的布局。

子類布局對(duì)象有一些職責(zé)。集合視圖依靠這個(gè)子類布局對(duì)象來(lái)告訴它如何顯示其單元格。這是一個(gè)關(guān)鍵的概念。布局內(nèi)容并不是通過(guò)創(chuàng)建一個(gè) UICollectionView 的子類來(lái)實(shí)現(xiàn)的。雖然這是在子類UIScrollView 時(shí)布局子視圖的常見(jiàn)模式,但除非絕對(duì)必要,我們要避免創(chuàng)建 UICollectionView 子類。

所以,集合視圖會(huì)向其布局對(duì)象詢問(wèn)如何布局其內(nèi)容的線索。當(dāng)一個(gè)集合視圖向用戶顯示內(nèi)容時(shí),實(shí)際發(fā)生的事件順序是什么?

首先,集合視圖詢問(wèn)其數(shù)據(jù)源,以獲得關(guān)于要向用戶顯示的內(nèi)容的信息。這包括要顯示多少組數(shù)據(jù)、每組數(shù)據(jù)要顯示的單元格、輔助視圖的數(shù)量。

接下來(lái),集合視圖從其布局對(duì)象中收集有關(guān)如何顯示單元格、輔助視圖和裝飾視圖的信息。這些信息存儲(chǔ)在一個(gè)名為 UICollectionViewLayoutAttributes 的類的實(shí)例對(duì)象中。

最后,集合視圖將有關(guān)布局的信息轉(zhuǎn)發(fā)給單元格、輔助視圖和裝飾視圖。這些類中的每一個(gè)類都負(fù)責(zé)使用它所得到的信息將這些布局屬性應(yīng)用到自己身上。推遲到父類的實(shí)現(xiàn),或者完全省略一個(gè)實(shí)現(xiàn),將確保已經(jīng)由集合視圖處理的布局屬性(如框架)得到應(yīng)用。你的實(shí)現(xiàn)應(yīng)該集中在你添加的任何自定義屬性上(但后面會(huì)有更多的介紹)。

每當(dāng)當(dāng)前布局失效時(shí),就會(huì)發(fā)生這些步驟,你可以通過(guò)在布局對(duì)象上調(diào)用 invalidateLayout 來(lái)強(qiáng)制執(zhí)行布局更新。

現(xiàn)在你已經(jīng)知道了布局內(nèi)容時(shí)使用的不同類:

  • UICollectionView 是向用戶展示內(nèi)容的視圖;
  • UICollectionViewCell 負(fù)責(zé)向用戶展示一個(gè)單元格的內(nèi)容;
  • UICollectionViewLayout 確定單元格的布局位置信息,并將這些信息返回給集合視圖;
  • UICollectionViewLayoutAttributes,它是一個(gè)布局存儲(chǔ)信息的類,要將這些信息調(diào)配給單元格、輔助視圖和裝飾視圖。

如果回過(guò)頭來(lái)看這些類,就會(huì)發(fā)現(xiàn)有一個(gè)明顯的劃分,哪些是參與數(shù)據(jù)和自身布局的,哪些是只負(fù)責(zé)布局的。圖4.1顯示了這種劃分。UICollectionView 從橙色框中的類中收集數(shù)據(jù)信息,并將其與藍(lán)色框中的類的布局信息相結(jié)合。

圖 4.1

請(qǐng)注意,布局對(duì)象對(duì)集合視圖的委托對(duì)象有一個(gè)間接的引用。這個(gè)連接可以被布局對(duì)象用來(lái)詢問(wèn)委托對(duì)象關(guān)于特定項(xiàng)目布局的信息。例如,UICollectionViewDelegateFlowLayout 協(xié)議擴(kuò)展了UICollectionViewDelegate,并被 UICollectionViewFlowLayout 用來(lái)詢問(wèn)委托對(duì)象關(guān)于特定 item 的布局信息。這個(gè)話題很復(fù)雜,但你已經(jīng)在上一章看到了一個(gè)例子,當(dāng)委托對(duì)象為不同的 item 指定單獨(dú)的 size 時(shí)。你將在后面看到一個(gè)進(jìn)一步擴(kuò)展這個(gè)功能的例子。

我們已經(jīng)介紹了基礎(chǔ)知識(shí):什么是布局,它有什么作用,以及它如何與集合視圖架構(gòu)的其他部分進(jìn)行交互。到目前為止,這已經(jīng)是非常學(xué)術(shù)性的內(nèi)容了。讓我們來(lái)看看一些代碼。

創(chuàng)建 UICollectionViewFlowLayout 子類

我們已經(jīng)看到很多復(fù)雜的行為和布局是使用內(nèi)置的 UICollectionViewFlowLayout 生成的,那么為什么會(huì)選擇將其子類化呢?原因有很多。

  • 要修改你的子類的布局的屬性,這超出了委托方法所能實(shí)現(xiàn)的范圍;
  • 在你的布局中加入裝飾視圖;
  • 增加新的輔助視圖;
  • 要擴(kuò)展 UICollectionViewLayoutAttributes,為你的布局類添加新的項(xiàng)目屬性來(lái)管理;
  • 要添加手勢(shì)支持;
  • 要自定義插入、更新和刪除更新到集合視圖的動(dòng)畫。

除了在第 6 章 "為 UICollectionView 添加交互性 "中涵蓋的手勢(shì)支持外,我們將針對(duì)每個(gè)原因的子類流式布局看代碼示例。

讓我們回顧 Survey 示例——它的代碼在 Better Survey 中。有幾種方法可以讓它變得更好,第一種方法如圖 4.2 所示。因?yàn)椴皇撬械膯卧穸加邢嗤拇笮。詥卧癫粫?huì)再垂直對(duì)齊。開(kāi)箱即用,UICollectionViewFlowLayout 并沒(méi)有提供對(duì)那種 "均勻間隔 "感覺(jué)的支持,我認(rèn)為這種感覺(jué)在這里會(huì)更好。幸運(yùn)的是,我們想要的東西屬于 "基于行的,打破布局 " 的流式布局,所以我想我們可以通過(guò)創(chuàng)建一個(gè) UICollectionViewFlowLayout 子類的方式來(lái)實(shí)現(xiàn)我們想要的視覺(jué)效果。

圖 4.2

在 Xcode 中創(chuàng)建一個(gè)名為 AFCollectionViewFlowLayout 的新類,它是 UICollectionViewFlowLayout 的子類對(duì)象。接下來(lái),我們可以把視圖控制器中的很多布局邏輯代碼移動(dòng)到該布局類中。

#import <UIKit/UIKit.h>

#define kMaxItemDimension   100.0f
#define kMaxItemSize        CGSizeMake(kMaxItemDimension, kMaxItemDimension)

extern NSString * const AFCollectionViewFlowLayoutBackgroundDecoration;

@interface AFCollectionViewFlowLayout : UICollectionViewFlowLayout

@end

可以看到,我們已經(jīng)將單元格最大尺寸的宏定義(kMaxItemSize)移動(dòng)到布局的頭文件中。這是個(gè)(比把它放在視圖控制器實(shí)現(xiàn)文件中)更合適的地方。

接下來(lái),我們將實(shí)現(xiàn) init 方法,并在里面設(shè)置布局參數(shù):

-(instancetype)init {
    if (!(self = [super init])) return nil;
    
    // 在初始化方法中設(shè)置默認(rèn)布局參數(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)建視圖控制器中的布局對(duì)象。使用 #import 導(dǎo)入AFCollectionViewFlowLayout 頭文件,將布局和集合視圖的創(chuàng)建方式改為清單 4.3 所示的代碼。

// 創(chuàng)建一個(gè)基礎(chǔ)流式布局,以自適應(yīng)縱向的三列
AFCollectionViewFlowLayout *surveyFlowLayout = [[AFCollectionViewFlowLayout alloc] init];
    
// 用自定義的流式布局創(chuàng)建一個(gè)新的集合視圖,并設(shè)置委托對(duì)象和數(shù)據(jù)源對(duì)象
UICollectionView *surveyCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:surveyFlowLayout];

通過(guò)將布局參數(shù)的相關(guān)設(shè)置移動(dòng)到布局對(duì)象本身的初始化方法中,我們?cè)谝晥D控制器中寫的代碼就少了很多。此外,如果我們重用該布局,我們就不會(huì)在兩個(gè)地方編寫重復(fù)的代碼。重用這個(gè)布局的視圖控制器總是可以進(jìn)一步自定義布局屬性,但他們不必這樣做。這是你在編寫自定義布局時(shí)的最佳實(shí)踐。

接下來(lái),需要在我們的 UICollectionViewFlowLayout 子類中重寫兩個(gè)方法,當(dāng)集合視圖在布局其單元格、輔助視圖和裝飾視圖時(shí),這些方法將被調(diào)用。這兩個(gè)方法是 layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:。我們還要?jiǎng)?chuàng)建第三個(gè)私有方法,叫做 applyLayoutAttributes:,我們?cè)诤竺嬗懻?。這兩個(gè)被覆蓋的方法都會(huì)調(diào)用這個(gè)自定義方法(見(jiàn)清單 4.4)。

/**
 該方法返回一個(gè)包含所有布局信息 UICollectionViewLayoutAttributes 的數(shù)組。

 我們通過(guò)父類方法 [super layoutAttributesForElementsInRect:rect] 先創(chuàng)建了一個(gè)正常情況下的所有屬性的數(shù)組。
 這個(gè)父類方法默認(rèn)情況下,只會(huì)創(chuàng)建在 rect 范圍內(nèi)的視圖的布局屬性。
 所以,如果你想把原來(lái)不會(huì)被顯示的視圖也顯示出來(lái)的話,你就不得不自己把所有布局屬性都創(chuàng)建出來(lái),放入數(shù)組中。
*/
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    
    NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];
    
    // 該數(shù)組中存放我們?cè)诿總€(gè) 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;
}

這兩個(gè)方法所做的第一件事就是調(diào)用它們父類的實(shí)現(xiàn)。通過(guò)這樣做,我們免費(fèi)獲得了所有的 UICollectionViewFlowLayout 默認(rèn)行為。在我們檢索到默認(rèn)屬性后,再調(diào)整各個(gè) item 的布局。

現(xiàn)在我們來(lái)看看 applyLayoutAttributes: 方法。我們首先檢查布局屬性的 representedElementKind 屬性。對(duì)于普通的 UICollectionViewCell 來(lái)說(shuō),這將是 nil。否則,它將是集合視圖注冊(cè)的輔助視圖類型;在我們的例子中,它將是UICollectionElementKindSectionHeader。還有一點(diǎn)值得記住,center 和 size 分別定義了一個(gè) item 的 position 和 size。當(dāng)計(jì)算這些時(shí),你可能最終會(huì)在半像素上渲染視圖,使它們變得模糊不清。frame 屬性是一種方便的方法,用于訪問(wèn)布局屬性的大小和中心。通過(guò)將框架設(shè)置為自身的 CGRectIntegral(見(jiàn)清單4.5),我們可以確保視圖不會(huì)呈現(xiàn)在像素邊界上。

// 修改并更新每一個(gè) item 的位置
-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes {
    
    // 對(duì)于一個(gè)普通的 UICollectionViewCell 來(lái)說(shuō),它的 representedElementKind 值為 nil
    // 檢查 representedElementKind 是否為 nil,表明這是一個(gè)單元格,而不是一個(gè) 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ù)量的項(xiàng)目,而不是只有三個(gè)。

圖 4.3

啊,你知道這本電子書最終會(huì)有一些數(shù)學(xué)的內(nèi)容! 但是,其實(shí)并沒(méi)有那么復(fù)雜。

如果我們?cè)龠\(yùn)行這個(gè)應(yīng)用程序,我們會(huì)看到單元格是均勻分布的,如圖4.4所示。

圖4.4

現(xiàn)在我們已經(jīng)把我們的單元格排列成一個(gè)漂亮的網(wǎng)格模式,讓我們添加一個(gè)裝飾視圖。裝飾視圖是對(duì) UICollectionView 的數(shù)據(jù)驅(qū)動(dòng)內(nèi)容的視覺(jué)補(bǔ)充。它們并不顯示單元格的信息;相反,它們伴隨著單元格的視覺(jué)效果:設(shè)計(jì)師最好的朋友。

我不是設(shè)計(jì)師,但我已經(jīng)成功地想出了一個(gè)文件夾的想法。我們的應(yīng)用要炫耀這股 "扁平化設(shè)計(jì) "的熱潮,將我們的照片擺放在一個(gè)三環(huán)的文件夾上面。我拍了一張文件夾的照片,然后把它拉長(zhǎng)。我們要讓這個(gè)裝飾視圖鋪在每一排照片的后面。

因?yàn)檠b飾視圖不是數(shù)據(jù)驅(qū)動(dòng)的,所以不會(huì)向視圖控制器添加任何代碼。相反,裝飾視圖的所有代碼都將存在于我們的 AFCollectionViewFlowLayoutUICollectionReusableView 的一個(gè)子類中。

這個(gè)類 UICollectionReusableViewAFCollectionHeaderView 甚至UICollectionViewCell 的父類。它提供了重用集合視圖中任何特定視圖的通用邏輯,其中包括單元格、補(bǔ)充視圖和裝飾視圖。因?yàn)檫@些類可以重用,我們可以把已經(jīng)學(xué)到的關(guān)于重用的知識(shí)應(yīng)用到裝飾視圖中?,F(xiàn)在就讓我們這樣做。

創(chuàng)建一個(gè)新的類,父類是 UICollectionReusableView。我把我的類叫做 AFDecorationView。它沒(méi)有任何屬性,而且它的實(shí)現(xiàn)看起來(lái)相當(dāng)無(wú)聊(見(jià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

這個(gè)類所做的就是,當(dāng)初始化時(shí),在它的視圖層次結(jié)構(gòu)中添加一個(gè)UIImageView,里面有我們的 "binder "圖像。沒(méi)有必要覆蓋 prepareForReuse 方法,因?yàn)樵谖覀兊难b飾視圖中沒(méi)有特定的數(shù)據(jù)內(nèi)容。

現(xiàn)在我們已經(jīng)創(chuàng)建了裝飾視圖子類,讓我們把它添加到集合視圖中。這比 header 視圖要棘手一些,因?yàn)?UICollectionView 并沒(méi)有為我們內(nèi)置任何內(nèi)容,我們需要自己構(gòu)建一切。

將裝飾視圖的頭文件導(dǎo)入到布局子類中。修改 layoutAttributesForElementsInRect: 方法的實(shí)現(xiàn),使其看起來(lái)像清單4.7。

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    
    NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];
    
    // 該數(shù)組中存放我們?cè)诿總€(gè) section 中新增的「裝飾視圖」布局參數(shù)
    NSMutableArray *newAttributesArray = [NSMutableArray array];
    for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
        [self applyLayoutAttributes:attributes];
        
        // 默認(rèn)情況下,「裝飾視圖」不會(huì)被顯示,所以需要?jiǎng)?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 語(yǔ)句。我們想在每個(gè)部分添加一個(gè)裝飾視圖,而每個(gè)部分只有一個(gè) header,所以我們將搭載這個(gè)邏輯來(lái)添加我們的輔助視圖。

代碼本身可能看起來(lái)有點(diǎn)奇怪。請(qǐng)記住,layoutAttributesForElementsInRect: 是為所有類型的元素調(diào)用的,而不僅僅是單元格。因此,當(dāng)它被調(diào)用到我們的 header 視圖時(shí),我們的 if 語(yǔ)句評(píng)估為 YES,我們就會(huì)創(chuàng)建一個(gè)新的布局屬性。我們返回的數(shù)組將包含這個(gè)新屬性。

接下來(lái),我們需要為 layoutAttributesForDecorationViewOfKind:atIndexPath: 實(shí)現(xiàn)一個(gè)方法,因?yàn)槟J(rèn)的實(shí)現(xiàn)會(huì)返回 nil,當(dāng)我們?cè)噲D將它添加到我們的可變字典中時(shí),我們的應(yīng)用程序會(huì)崩潰。

我們需要實(shí)現(xiàn)一個(gè)方法,該方法將創(chuàng)建一個(gè)新的 UICollectionViewLayoutAttributes 對(duì)象,并自定義它的屬性,以便裝飾視圖將適合我們的單元格內(nèi)容后面(見(jiàn)清單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);

        /**
         默認(rèn)情況下,單元格的 zIndex 值為 0,
         將裝飾視圖的 zIndex 值設(shè)置為 -1,可以將「裝飾視圖」顯示在單元格的視圖層次后面。
         */
        layoutAttributes.zIndex = -1;
    }
    
    return layoutAttributes;
}

本示例使用方法 layoutAttributesForDecorationViewOfKind:withIndexPath: 創(chuàng)建一個(gè)新的 UICollectionViewLayoutAttributes 對(duì)象。然后,它根據(jù)我們要找的東西來(lái)定制屬性中的屬性。我們希望我們的裝飾視圖以其部分中最高的項(xiàng)目為垂直中心,所以我們需要循環(huán)處理每一個(gè)項(xiàng)目。幸運(yùn)的是,檢索這些屬性的邏輯已經(jīng)在 layoutAttributesForItemAtIndexPath: 中實(shí)現(xiàn)了。當(dāng)我們向我們的超級(jí)類詢問(wèn)給定單元格的屬性時(shí),它會(huì)查詢集合視圖委托的大小(代碼我們已經(jīng)寫好了)。

我們可以利用這個(gè)現(xiàn)有的功能來(lái)處理繁重的工作。我們并沒(méi)有計(jì)算裝飾視圖的中心,實(shí)際上,我們只是依靠最高物品的垂直中心,它已經(jīng)為我們計(jì)算好了。萬(wàn)歲!

所以,在我們定義了裝飾視圖的大小和高度之后,我們需要設(shè)置它的 zIndex 屬性。這將告訴集合視圖以何種順序呈現(xiàn)其項(xiàng)目。重疊但具有相同 zIndex 的項(xiàng)目有一個(gè)未定義的渲染順序。我們希望裝飾視圖渲染在所有單元格后面,而這些單元格的默認(rèn) zIndex 為 0,所以我們將裝飾視圖的 zIndex 設(shè) 置為 -1。

我們需要做的唯一一件事就是將我們的裝飾視圖類注冊(cè)到布局類中(見(jiàn)清單 4.9)。我們將在AFCollectionViewFlowLayoutinit方法中添加下面高亮顯示的一行。

-(instancetype)init {
    if (!(self = [super init])) return nil;
    
    // 在初始化方法中設(shè)置默認(rèn)布局參數(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);
    
    // !!!: 注冊(cè)裝飾視圖
    [self registerClass:[AFDecorationView class] forDecorationViewOfKind:AFCollectionViewFlowLayoutBackgroundDecoration];
        
    return self;
}

Surprise!圖 4.5 顯示,我們幾乎達(dá)到了目的。最后,我認(rèn)為這個(gè)演示程序可以使用一些漂亮的動(dòng)畫。UICollectionViewLayout 中已經(jīng)內(nèi)置了對(duì)動(dòng)畫的支持,我們只需要實(shí)現(xiàn)一些方法。

圖 4.5

當(dāng)一個(gè)新的 item 被添加或更新到集合視圖中時(shí),initialLayoutAttributesForAppearingItemAtIndexPath: 方法就會(huì)被調(diào)用。我們可以通過(guò)它在動(dòng)畫開(kāi)始時(shí)為 item 提供初始布局屬性,集合視圖將把可動(dòng)畫的屬性,如 framealpha,插值到它們的正常位置。還有一個(gè)對(duì)應(yīng)的方法叫做finalLayoutAttributesForDisappearingItemAtIndexPath: 用于通過(guò)動(dòng)畫方式從集合視圖中移除 item。

不過(guò)我們可以動(dòng)畫的不僅僅是 item 元素。輔助視圖和裝飾視圖都有相應(yīng)的出現(xiàn)/消失方法。UICollectionViewLayout 的默認(rèn)實(shí)現(xiàn)返回 nil,表示簡(jiǎn)單的交叉淡化(crossfade)動(dòng)畫。我們也可以返回 nil 來(lái)使用交叉淡化動(dòng)畫。

剩下的問(wèn)題是,當(dāng)我們插入一個(gè)新的 section 時(shí),其他 section 也會(huì)被重新加載。這會(huì)導(dǎo)致不僅僅是出現(xiàn)的 secion 有動(dòng)畫。我們還需要限制哪些 section 會(huì)執(zhí)行動(dòng)畫。

在對(duì)集合視圖進(jìn)行任何更新之前,prepareForCollectionViewUpdates: 被調(diào)用,其參數(shù)是一個(gè) UICcollectionViewUpdateItem 對(duì)象數(shù)組。這些是即將發(fā)生的更新。在它們完成后,調(diào)用 finalizeCollectionViewUpdates。這些都是成對(duì)的。我們將創(chuàng)建一個(gè)實(shí)例變量NSMutableSet 來(lái)獲取正在插入的 section。我們使用 set 是因?yàn)樗哂泻銜r(shí)查找功能(見(jiàn)清單4.10)。

@implementation AFCollectionViewFlowLayout
{
    NSMutableSet *insertedSectionSet;
}

-(instancetype)init {
    if (!(self = [super init])) return nil;
    
    // 在初始化方法中設(shè)置默認(rèn)布局參數(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);
    
    // !!!: 注冊(cè)裝飾視圖
    [self registerClass:[AFDecorationView class] forDecorationViewOfKind:AFCollectionViewFlowLayoutBackgroundDecoration];
    
    insertedSectionSet = [NSMutableSet set];
    
    return self;
}

現(xiàn)在我們只需要實(shí)現(xiàn) prepareForCollectionViewUpdates:finalizeCollectionViewUpdates 方法來(lái)更新集合視圖。對(duì)于這些方法,始終調(diào)用你的 super 實(shí)現(xiàn)是非常重要的(見(jiàn)清單4.11)。

#pragma mark Animation Support

-(void)prepareForCollectionViewUpdates:(NSArray *)updateItems {
    [super prepareForCollectionViewUpdates:updateItems];
    
    [updateItems enumerateObjectsUsingBlock:^(UICollectionViewUpdateItem *updateItem, NSUInteger idx, BOOL *stop) {
        // 如果當(dāng)前的 item 動(dòng)作為 Insert,則記錄到 NSMutableSet 集合中
        if (updateItem.updateAction == UICollectionUpdateActionInsert) {
            [insertedSectionSet addObject:@(updateItem.indexPathAfterUpdate.section)];
        }
    }];
}

-(void)finalizeCollectionViewUpdates {
    [super finalizeCollectionViewUpdates];
    
    // 當(dāng)更新完成后,從可變集中刪除所有項(xiàng)目,將其重置為空狀態(tài),以便進(jìn)行下一批更新。
    [insertedSectionSet removeAllObjects];
}

你可以看到,當(dāng)我們準(zhǔn)備更新時(shí),我們的布局會(huì)檢查更新動(dòng)作,看看是否是一個(gè)正在插入的 item。如果是,它就會(huì)向集合中添加一個(gè) NSNumber 實(shí)例,代表 item 在 section 中的索引。在集合(NSSet)中,重復(fù)的 item 會(huì)被忽略,所以我們不必檢查它是否已經(jīng)存在。

當(dāng)更新完成后,我們從可變集中刪除所有項(xiàng)目,將其重置為空狀態(tài),以便進(jìn)行下一批更新。

現(xiàn)在,我們已經(jīng)完成了這些工作,讓我們來(lái)看看在 item 和裝飾視圖中的動(dòng)畫代碼,如清單4.12所示。

// 自定義動(dòng)畫,添加裝飾視圖
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingDecorationElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)decorationIndexPath {
    // 返回 nil 則執(zhí)行默認(rèn)的 crossfade 動(dòng)畫
    
    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;
}

// 自定義動(dòng)畫,添加 item
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
    // 返回 nil 則執(zhí)行默認(rèn)的 crossfade 動(dòng)畫
    
    UICollectionViewLayoutAttributes *layoutAttributes;
    if ([insertedSectionSet containsObject:@(itemIndexPath.section)]) {
        layoutAttributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        layoutAttributes.transform3D = CATransform3DMakeTranslation([self collectionViewContentSize].width, 0, 0);
    }
    
    return layoutAttributes;
}

因?yàn)槟J(rèn)的實(shí)現(xiàn)返回 nil,所以我們不必?fù)?dān)心調(diào)用 super 關(guān)鍵字以調(diào)用父類實(shí)現(xiàn)。

這兩個(gè)實(shí)現(xiàn)是相似的,因?yàn)樗鼈儤?gòu)造的動(dòng)畫非常相似。對(duì)于裝飾視圖,我們檢查確保裝飾視圖是我們?cè)O(shè)置的那個(gè);雖然沒(méi)有其他裝飾視圖,但這是很好的實(shí)踐,以防我們以后添加更多的裝飾視圖。

無(wú)論在哪種情況下,我們都要檢查確保索引路徑的 item 部分是否包含在我們插入的部分集合中。如果是,我們從我們之前實(shí)現(xiàn)的 layoutAttributesForItemAtIndexPath:layoutAttributesForDecorationViewOfKind:atIndexPath: 中抓取一個(gè)UICollectionViewLayoutAttributes 的實(shí)例--我們?cè)诶梦覀円呀?jīng)寫好的代碼。

然后,我們?cè)O(shè)置一個(gè)變換,將裝飾視圖向左移動(dòng),將單元格向右移動(dòng),使它們?cè)趧?dòng)畫開(kāi)始時(shí)完全脫離可見(jiàn)的集合視圖。我們還將裝飾視圖的alpha 設(shè)置為零,這樣它就會(huì)漸漸消失。

現(xiàn)在,每當(dāng)插入一個(gè)新的部分,用戶就會(huì)看到文件夾從左邊移入,而照片從右邊移入。這是一個(gè)非常好的觸動(dòng)。

從這一節(jié)中,你應(yīng)該有一個(gè)關(guān)鍵的架構(gòu)啟示,那就是編寫 UICollectionViewFlowLayout子類就是盡可能地依賴現(xiàn)有的代碼。如果你發(fā)現(xiàn)自己要做復(fù)雜的數(shù)學(xué)計(jì)算一些已經(jīng)布局好的東西,請(qǐng)檢查是否有一些方法可以訪問(wèn)這些信息。

使用自定義屬性布局 item

UICollectionViewLayoutAttributes是一個(gè)類,這意味著我們可以對(duì)它進(jìn)行子類化。為什么我們要這么做呢?當(dāng)然是為了增加對(duì)更多屬性的支持! 讓我們來(lái)看看我的意思。

該類包含以下屬性,它們?cè)谶\(yùn)行時(shí)應(yīng)用于項(xià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)

以上這些屬性非常棒,你可以用它們來(lái)完成很多事情。但是有時(shí)候,你可能想添加自己的屬性。

這就是我們現(xiàn)在要做的。

這個(gè)項(xiàng)目在示例代碼中叫做 Dimensions。它已經(jīng)完成了一些圖像和模型的設(shè)置,我在這里不做介紹。它要解決的問(wèn)題是,照片有時(shí)在拉伸到縱橫向填充時(shí)看起來(lái)是最好的,裁剪圖像中多余的部分以適合它的容器。其他時(shí)候,你想使用縱橫適配,它將縮小圖像,使整個(gè)圖像在一個(gè)容器中可見(jiàn)。我們將編寫一個(gè)布局,作為布局屬性來(lái)處理這個(gè)問(wèn)題。

我用 Single View application 模板創(chuàng)建了一個(gè)新的 Xcode 項(xiàng)目。在刪除了.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è)置一個(gè)導(dǎo)航控制器,我們稍后將實(shí)現(xiàn)這個(gè)控制器。注意,我必須將 viewController 屬性的類型改為通用的 UIViewController

現(xiàn)在我們已經(jīng)在屏幕上有了我們的視圖控制器,我們可以設(shè)置集合視圖和布局了(見(jiàn)清單4.14)。

@implementation AFViewController
{
    // 數(shù)組模型對(duì)象
    NSArray *photoModelArray;
    
    UISegmentedControl *aspectChangeSegmentedControl;
    
    AFCollectionViewFlowLayout *photoCollectionViewLayout;
}

-(void)loadView {
    
    // 創(chuàng)建自定義布局對(duì)象實(shí)例
    photoCollectionViewLayout = [[AFCollectionViewFlowLayout alloc] init];
    
    // 創(chuàng)建自定義集合視圖
    UICollectionView *photoCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:photoCollectionViewLayout];
    
    photoCollectionView.dataSource = self;
    photoCollectionView.delegate = self;
    
    // 注冊(cè)重用 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)該是你熟悉的代碼了。注意,我們禁用了集合視圖中所有單元格的選擇交互。我們還有一個(gè)分段控件作為實(shí)例變量。這個(gè)控件將放在導(dǎo)航欄中,這樣用戶就可以在縱橫交錯(cuò)和縱橫填充之間進(jìn)行選擇。

我們稍后將實(shí)現(xiàn) loadView 中引用的 AFCollectionViewFlowLayout 類,但我們先看看視圖控制器的其他代碼。它在我們的導(dǎo)航欄中設(shè)置了分段控件(見(jiàn)清單4.15)。

-(void)viewDidLoad {
    [super viewDidLoad];
    
    // 在導(dǎo)航欄上添加自定義 UISegmentedControl 對(duì)象
    aspectChangeSegmentedControl = [[UISegmentedControl alloc] initWithItems:@[@"Aspect Fit", @"Aspect Fill"]];
    aspectChangeSegmentedControl.selectedSegmentIndex = 0;
    [aspectChangeSegmentedControl addTarget:self action:@selector(aspectChangeSegmentedControlDidChangeValue:) forControlEvents:UIControlEventValueChanged];
    self.navigationItem.titleView = aspectChangeSegmentedControl;
}

視圖控制器的其余實(shí)現(xiàn)是非常標(biāo)準(zhǔn)的(見(jià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;
}

在我們的視圖控制器中剩下的最后一個(gè)方法將是響應(yīng)用戶與分段控件交互的方法(見(jiàn)清單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:^{
        
        // 在兩種布局方式之間進(jìn)行切換
        if (self->photoCollectionViewLayout.layoutMode == AFCollectionViewFlowLayoutModeAspectFill) {
            self->photoCollectionViewLayout.layoutMode = AFCollectionViewFlowLayoutModeAspectFit;
        } else {
            self->photoCollectionViewLayout.layoutMode = AFCollectionViewFlowLayoutModeAspectFill;
        }
    }];
}

我們還沒(méi)有定義 layoutMode 屬性,所以我們現(xiàn)在就去做。這就是自定義布局屬性子類的作用。我們要添加一個(gè)新的布局屬性來(lái)指定照片的縮放模式。創(chuàng)建一個(gè)新的類,它是 UICollectionViewLayoutAttributes的子類(見(jiàn)清單4.18)。

typedef enum : NSUInteger{
    AFCollectionViewFlowLayoutModeAspectFit,    //Default
    AFCollectionViewFlowLayoutModeAspectFill
}AFCollectionViewFlowLayoutMode;

@interface AFCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes

@property (nonatomic, assign) AFCollectionViewFlowLayoutMode layoutMode;

@end

這就是我們真正需要的所有東西--布局模式的定義和一個(gè)持有它們的屬性。然而,看看 UICollectionViewLayoutAttributes 的定義;注意它遵守 NSCopying 協(xié)議。非常重要的是,我們也要遵守這個(gè)協(xié)議并實(shí)現(xiàn)copyWithZone 方法。(見(jiàn)清單4.19)。否則,我們的屬性將始終為零(編譯器保證的)。在iOS 7中的新功能。你現(xiàn)在必須在子類化布局屬性時(shí)覆蓋 isEqual: 方法。

#import "AFCollectionViewLayoutAttributes.h"

@implementation AFCollectionViewLayoutAttributes

-(id)copyWithZone:(NSZone *)zone {
    AFCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
    attributes.layoutMode = self.layoutMode;
    return attributes;
}

@end

現(xiàn)在我們可以實(shí)現(xiàn)我們的流式布局子類了。我創(chuàng)建了一個(gè)名為AFCollectionViewFlowLayout 的新類,它是UICollectionViewFlowLayout 的子類。它如清單 4.20 所示,從本章前面展示的改進(jìn)的 Survey 應(yīng)用中應(yīng)該看起來(lái)很熟悉。

#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

我們所做的是擴(kuò)展 UICollectionViewDelegateFlowLayout 協(xié)議來(lái)創(chuàng)建我們自己的布局。就像我們?yōu)?Survey 應(yīng)用定制單個(gè)單元格的大小一樣,我們希望提供一個(gè)接口,讓使用我們布局的開(kāi)發(fā)人員可以為他們單元格中的照片指定單獨(dú)的縱橫比。

現(xiàn)在我們已經(jīng)有了我們的自定義布局屬性類,讓我們簡(jiǎn)單地看看我們的自定義布局的部分,你應(yīng)該已經(jīng)熟悉了(見(jiàn)清單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;
}

這與我們?cè)诒菊虑懊娴牡谝粋€(gè)流式布局子類中看到的代碼是一樣的,不同的是我們使用的是 AFCollectionViewLayoutAttributes 而不是 UICollectionViewLayoutAttributes,而且我們還傳遞了我們的 layoutMode。

applyLayoutAttributes: 中,我們檢查集合視圖的委托,看它是否響應(yīng)我們?cè)?code>AFCollectionViewDelegateFlowLayout 協(xié)議中定義的選擇器。如果它響應(yīng)了,我們就把它投給一個(gè)符合協(xié)議的 id,這樣我們就可以從它那里抓取布局模式。

觀察敏銳的讀者可能會(huì)問(wèn)自己,集合視圖是如何知道使用我們自定義的UICollectionViewLayoutAttributes子類的。答案很簡(jiǎn)單。我們的布局需要實(shí)現(xiàn)一個(gè)類方法來(lái)告訴集合視圖使用哪個(gè)自定義類(見(jiàn)清單4.22)。顯然,默認(rèn)的實(shí)現(xiàn)會(huì)返回UICollectionViewLayoutAttributes。

+(Class)layoutAttributesClass
{
    // Important for letting UICollectionView know what kind of attributes to use.
    return [AFCollectionViewLayoutAttributes class];
}

唯一缺少的另一個(gè)組件是我們的布局可能最終處于無(wú)效狀態(tài)。如果我們改變布局模式而不更新已經(jīng)在屏幕上布局的單元格,已經(jīng)顯示的單元格將仍然應(yīng)用舊的布局,而由于滾動(dòng)或插入而變得可見(jiàn)的單元格將擁有新的布局。我們需要的是,每當(dāng)我們的布局模式發(fā)生變化時(shí),就調(diào)用 invalidateLayout 方法。

-(void)setLayoutMode:(AFCollectionViewFlowLayoutMode)layoutMode {
    // Update our backing ivar...
    _layoutMode = layoutMode;
    
    // 然后使我們舊的布局無(wú)效。
    [self invalidateLayout];
}

我知道我們已經(jīng)寫了很多代碼,但沒(méi)有任何回報(bào),但請(qǐng)?jiān)偃棠鸵幌?。即使我們有了我們的自定義布局,并且正在設(shè)置自定義屬性,我們?nèi)匀粵](méi)有任何代碼將該屬性應(yīng)用到單元格中。我創(chuàng)建了一個(gè) UICollectionViewCell 的子類 AFCollectionViewCell。它顯示由其setImage: 方法設(shè)置的圖像。清單 4.24 所示的實(shí)現(xiàn),與第 3 章的 Survey 應(yīng)用程序中使用的實(shí)現(xiàn)幾乎相同。然而,存在兩個(gè)關(guān)鍵的區(qū)別。

首先,我們?yōu)椴季帜J铰暶髁艘粋€(gè)實(shí)例變量,其次,我們?cè)谝粋€(gè)新的方法中使用該實(shí)例變量來(lái)設(shè)置圖像視圖的 frame。這個(gè)問(wèn)題與 iOS 7 中引擎的變化有關(guān);現(xiàn)在方法的調(diào)用順序不同,所以每當(dāng)設(shè)置一個(gè)新的圖像時(shí),設(shè)置圖像的 frame 是很重要的(這很有意義,因?yàn)閳D像的 frame 取決于圖像的縱橫比,而我們?cè)谠O(shè)置 UIImage 實(shí)例之前是不知道的)。

@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 的中心點(diǎn)
        imageView.center = CGPointMake(CGRectGetMinX(self.bounds), CGRectGetMidY(self.bounds));
    }
}

@end

重要的是,圖像視圖的 clipsToBounds 屬性被設(shè)置為 YES。這就確保了當(dāng)照片被縮放以適合于圖像視圖,并裁剪自身的一部分時(shí),被裁剪的區(qū)域?qū)⒉豢梢?jiàn)。

接下來(lái),我們有代碼來(lái)實(shí)際應(yīng)用布局模式到單元格中(見(jiàn)清單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));
}

這個(gè)方法屬于 UICollectionReusableView,因?yàn)椴季謱傩赃m用于單元格 item、輔助視圖和裝飾視圖。首先,你必須調(diào)用 super 覆蓋父類的實(shí)現(xiàn)。接下來(lái),它檢查確保布局屬性是我們自定義子類的一個(gè)實(shí)例,然后再投遞指針。

我們使用布局模式來(lái)決定是否應(yīng)該將圖像視圖的大小設(shè)置為我們的 bounds 大小,或者是否應(yīng)該調(diào)整它。如果模式是縱橫適配,我們使用類似于第3章 "內(nèi)容上下文化 "中的調(diào)查視圖控制器的邏輯來(lái)調(diào)整它。最后,我們?cè)O(shè)置圖像視圖的邊界和中心。我們使用大小和位置而不是 contentMode,這樣我們就可以很容易地從一種模式過(guò)渡到另一種模式的動(dòng)畫。boundscenter 是隱式的可動(dòng)畫屬性)。

最后,在所有這些代碼之后,你可以運(yùn)行應(yīng)用程序,并在縱橫向適合和縱橫向填充照片之間進(jìn)行過(guò)渡(見(jiàn)圖 4.6)。它將對(duì)過(guò)渡進(jìn)行動(dòng)畫處理,即使是滾動(dòng)或旋轉(zhuǎn)的動(dòng)畫。

Aspect Fit Aspect Fill

...

網(wǎng)格視圖之外

到目前為止,我們看到的流式布局所做的都是網(wǎng)格視圖的一些變化。雖然網(wǎng)格布局的確是一種基于線條的、打破常規(guī)的布局方式,但它只是這種布局的一種特殊情況。讓我們更進(jìn)一步,做一些真正有趣的事情。

我們要實(shí)現(xiàn)一個(gè)封面流布局。在此之前,我要特別感謝 Mark Pospesel 在 GitHub 上建立了他的 Introducing Collection Views 項(xiàng)目。我書中這一節(jié)的代碼大量借鑒了他的例子,經(jīng)他許可使用。本節(jié)的示例代碼可以以 Cover Flow 的名義獲得。

創(chuàng)建一個(gè)標(biāo)準(zhǔn)的 “Single-View Xcode 項(xiàng)目并刪除.xib文件" 之后,我們要做的第一件事是在項(xiàng)目導(dǎo)航器窗格中打開(kāi)項(xiàng)目設(shè)置。在 "Build Phases"中,展開(kāi) "Link Binary with Libraries "并點(diǎn)擊加號(hào)。選擇并添加 QuartzCore 框架,打開(kāi) Supporting Files 組下的 Prefix 文件。我的叫 CoverFlowPrefix.pch;它是一個(gè)頭文件,會(huì)被導(dǎo)入到所有的頭文件中。添加 #import <QuartzCore/QuartzCore.h> 到 PCH 中?,F(xiàn)在我們可以在整個(gè)項(xiàng)目中訪問(wèn)所有的 QuartzCore 框架。我們以后會(huì)需要這個(gè)來(lái)使用 CALayer。這一步對(duì)我來(lái)說(shuō)是創(chuàng)建 Xcode 項(xiàng)目中很常見(jiàn)的一步,蘋果公司默認(rèn)不包含它真是個(gè)奇跡。

視圖控制器將與 Dimensions 非常相似,只是這次我們將有兩個(gè)布局。我們將像上次一樣,在導(dǎo)航欄中使用一個(gè)分段控件來(lái)切換這兩種布局(見(jiàn)清單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)建了兩個(gè)布局對(duì)象,一個(gè)是自定義的 AFCoverFlowFlowLayout,另一個(gè)是 UICollectionViewFlowLayout。
     通過(guò) UISegmentedControl 進(jìn)行布局方式的切換
     */
    // 初始化自定義集合視圖布局,封面流布局
    coverFlowCollectionViewLayout = [[AFCoverFlowFlowLayout alloc] init];
    
    // 創(chuàng)建一個(gè)基本的流程布局,將在縱向容納三列
    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é)中使用的方法完全相同,因此這里沒(méi)有顯示它們。然而,我們將實(shí)現(xiàn)一個(gè)新的UICollectionViewDelegateFlowLayout 方法,它將負(fù)責(zé)返回我們布局的邊緣插入量(見(jiàn)清單4.27)。我們使用這種方法是因?yàn)?Cover Flow 布局需要不同的 section 邊緣插入量,這取決于接口的方向和它運(yùn)行的具體設(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);
            }
        }
    }
}

這些值主要是通過(guò)實(shí)驗(yàn)來(lái)確定的,看看什么看起來(lái)是正確的。我鼓勵(lì)你采取這種方法,而不是用數(shù)學(xué)方法來(lái)預(yù)測(cè)它們,原因很簡(jiǎn)單,如果你的用戶看起來(lái)不正確,那么某件事在數(shù)學(xué)上是否正確并不重要。

最后,我們需要實(shí)現(xiàn)我們的用戶交互代碼。如清單4.28所示,你會(huì)發(fā)現(xiàn)它與上一個(gè)例子類似。

// !!!: 動(dòng)態(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];
}

我們明確地不對(duì)布局的變化進(jìn)行動(dòng)畫處理,因?yàn)樗鼈冎g的差別太大,它們之間的動(dòng)畫對(duì)用戶來(lái)說(shuō)顯得很刺眼。正如你在下一章中看到的那樣,用動(dòng)畫在布局之間進(jìn)行改變其實(shí)是很容易做到的。

在改變布局之后,我們需要將新布局無(wú)效化。雖然這一點(diǎn)沒(méi)有包含在文檔中,但我注意到,如果你省略了這一點(diǎn),一些布局會(huì)出現(xiàn)一些奇怪的行為。實(shí)驗(yàn)一下,看看什么對(duì)你的自定義布局有效。

我們將創(chuàng)建一個(gè)新的自定義 UICollectionViewLayoutAttributes 子類,以保存兩個(gè)值:一個(gè)用于指示我們是否應(yīng)該柵格化圖層,另一個(gè)用于指示單元格應(yīng)該如何 "遮擋"。我們不能使用 alpha,因?yàn)榘胪该鞯暮竺娴膯卧駮?huì) "滲入"。新的子類如清單4.29所示。對(duì)于我們的封面視圖布局,單元格將始終是柵格化的,因?yàn)榉駝t它們會(huì)因?yàn)?D變換而得到一些鋸齒狀的邊緣。

至于遮罩層,我們希望不在集合視圖中心的項(xiàng)目不要那么突出,所以我們會(huì)在每個(gè)單元格的頂部放置一個(gè)半透明的遮罩視圖。

//  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

開(kāi)啟 shouldRasterize 后,CALayer 會(huì)被光柵化為 bitmap,layer 的陰影等效果也會(huì)被保存到 bitmap 中。

接下來(lái),讓我們看看自定義的 UICollectionViewFlowLayout 子類本身(見(jiàn)清單4.30)。我省略了文件頂部的#定義,這些定義在后面會(huì)用到。我將把它們包含在那里。

@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;
}

// !!!: 返回自定義布局屬性對(duì)象
+(Class)layoutAttributesClass {
    return [AFCollectionViewLayoutAttributes class];
}

/**
 !!!: 當(dāng)用戶滾動(dòng)集合視圖時(shí),單元格的變換(在每一幀刷新時(shí))都會(huì)被重新計(jì)算
 */
-(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;
}

其中大部分是標(biāo)準(zhǔn)的流程布局代碼。然而,請(qǐng)注意,我們正在計(jì)算集合視圖中的可見(jiàn)矩形。這個(gè)矩形將被用來(lái)決定對(duì)每個(gè)單元格應(yīng)用多少3D變換和轉(zhuǎn)換。我們將通過(guò)獲取集合視圖的內(nèi)容偏移和邊界大小來(lái)輕松計(jì)算。

我們還在 shouldInvalidateLayoutForBoundsChange 中返回 YES,這樣當(dāng)用戶滾動(dòng)集合視圖時(shí),單元格的變換會(huì)被重新計(jì)算(在每一幀刷新時(shí))。

最小線間距(minimumLineSpacing)為負(fù)值,因?yàn)槲覀兿M覀兊膯卧?"捆綁 "在一起,而在水平滾動(dòng)的集合視圖中,線間距是每個(gè)垂直列單元格之間的距離。如圖 4.7 所示,行間距是按行間的空間計(jì)算的,而項(xiàng)目間的間距是沿行的單元格之間的空間。

圖 4.17

這可能是一個(gè)棘手的問(wèn)題,所以請(qǐng)記住,在垂直滾動(dòng)的集合視圖中,行間距和項(xiàng)目間的間距分別類似于寫作中的行高和內(nèi)核。在水平滾動(dòng)的集合視圖中,它們是翻轉(zhuǎn)的。

接下來(lái)是用于對(duì)我們的單元格應(yīng)用透視三維變換的密集數(shù)學(xué)計(jì)算(見(jiàn)清單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

/**
 !!!: 自定義布局,通過(guò) item 與中心點(diǎn)的距離執(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];
}

??!不要擔(dān)心,如果它看起來(lái)像很多。我將介紹更多細(xì)節(jié),你可以稍后再做具體實(shí)驗(yàn),畢竟這不是一本關(guān)于 CATransform3D 的書。最重要的是,你可以通過(guò)集合視圖在三維空間應(yīng)用變換??釘懒?

如果屬性的 itme 足夠接近可見(jiàn)區(qū)域的中心,第一個(gè)if分支就會(huì)執(zhí)行。它將根據(jù)它與中心的接近程度,給它進(jìn)行縮放、平移和三維透視變換。如果一個(gè) item 正好在中心,那么變換就不會(huì)有任何作用。

如果 item 離中心足夠遠(yuǎn),則 else 分支會(huì)執(zhí)行,以確保 item 不會(huì)變得過(guò)于變換。想象一下,在 Cover Flow 中,延伸到邊緣的 item 不斷地被應(yīng)用了越來(lái)越多的變換,它們最終會(huì)變得如此變換,以至于它們會(huì)翻轉(zhuǎn)到它們的另一邊!我們還想設(shè)置一個(gè)默認(rèn)的變換分支。

我們還要設(shè)置默認(rèn)的蒙版值為0,并始終將光柵化設(shè)置為YES?,F(xiàn)在讓我們運(yùn)行應(yīng)用程序,看看發(fā)生了什么。請(qǐng)注意,你可以非常容易地在普通流布局和封面流布局之間切換(見(jiàn)圖4.8)。

Boring Flow Corver Flow

它看起來(lái)很棒。然而,這有幾個(gè)問(wèn)題。首先,注意到集合視圖在單元格之間停了一半;在真正的 Cover Flow 中,滾動(dòng)視圖在一個(gè)項(xiàng)目完全居中的情況下停止。其次,你可以清楚地看到,我們的蒙版和柵格化的布局屬性沒(méi)有被應(yīng)用。嗯,那是因?yàn)槲覀冞€沒(méi)有應(yīng)用遮罩和光柵化的布局屬性。哦,那是因?yàn)槲覀冞€沒(méi)有寫出這樣的代碼。我們先來(lái)處理第一個(gè)問(wèn)題。

targetContentOffsetForProposedContentOffset:withScrollingVelocity: 是一個(gè)定義在 UICollectionViewLayout 中的方法,并且可以被重寫。

子類,包括我們的子類。它為子類提供了一個(gè)定義集合視圖將 "扣 "到哪里的機(jī)會(huì)。我們要實(shí)現(xiàn)它,并使用我們?cè)? layoutAttributesForElementsInRect: 中的現(xiàn)有代碼來(lái)獲取擬議矩形中元素的屬性(見(jiàn)清單4.32)。然后,我們將找到其項(xiàng)目將最接近擬議的可見(jiàn)矩形中心的屬性。然后,我們將找出該項(xiàng)目將有多遠(yuǎn),并返回一個(gè)調(diào)整后的內(nèi)容偏移,使該視圖居中。

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    // 返回一個(gè)我們想要讓集合視圖停止?jié)L動(dòng)的坐標(biāo)點(diǎn)
    
    // 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ū)⒖煺盏阶罱捻?xiàng)目。接下來(lái)讓我們實(shí)現(xiàn)我們的UICollectionViewCell子類。清單4.33中有完整的實(shí)現(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ùn)行應(yīng)用程序,看看其他單元格的 "淡入淡出 "效果和快照效果。

很好!玩一玩吧。實(shí)驗(yàn)旋轉(zhuǎn)和改變布局,同時(shí)集合視圖正在減速。找到它的能力和局限性。

現(xiàn)在,我們已經(jīng)實(shí)現(xiàn)了想要達(dá)到的效果,我想談?wù)勎野l(fā)現(xiàn)的集合視圖的幾個(gè)問(wèn)題。

首先,封面流視圖的旋轉(zhuǎn)動(dòng)畫并不完美。我似乎無(wú)法讓它實(shí)現(xiàn)無(wú)縫連接;我想這可能與旋轉(zhuǎn)過(guò)程中改變 contentSize 有關(guān)。

我最初嘗試在旋轉(zhuǎn)期間將布局改為 Cover Flow,這樣在縱向時(shí)使用普通流布局,在橫向時(shí)使用 Cover Flow 布局。在旋轉(zhuǎn)過(guò)程中改變布局非常麻煩,因?yàn)樵谛D(zhuǎn)過(guò)程中布局子類中的contentSize 不可靠,在改變布局時(shí)更不可靠。

我研究了這些問(wèn)題,找到了布局使用時(shí)的精確事件順序。

  1. prepareLayout 是在布局上調(diào)用的,這樣它就有機(jī)會(huì)進(jìn)行任何前期的計(jì)算。
  2. collectionViewContentSize 在布局上被調(diào)用,以確定集合視圖的內(nèi)容大小。
  3. layoutAttributesForElementsInRect: 被調(diào)用。

然后,布局激活,并繼續(xù)調(diào)用 layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:,直到布局變得無(wú)效。然后,再次重復(fù)這個(gè)過(guò)程。

在布局中使用 content size 可能不是一個(gè)好主意;UICollectionView 仍然是非常新的,社區(qū)仍在確定使用它的最佳實(shí)踐。

根據(jù)你對(duì)布局的想法,可能最好轉(zhuǎn)向 UICollectionViewLayout,就像我們?cè)谙乱徽伦龅哪菢?。然而,始終要先考慮 UICollectionViewFlowLayout 是否能完成你的目標(biāo)。它為你做了很多繁重的工作。

我們現(xiàn)在已經(jīng)涵蓋了裝飾視圖、集合視圖布局、布局屬性和自定義動(dòng)畫。你已經(jīng)鞏固了前三章的知識(shí),并為即將到來(lái)的一章浸入了水中。我們即將做一些非常有趣的事情,但首先,讓我們回顧一下 UITableView。

UITableView:UICollectionView 它爹

UICollectionView 是在 iOS 6 中才引入的,但 UITableView 從 2008 年最初的 iPhone SDK 發(fā)布時(shí)就已經(jīng)存在了。UITableView 所使用的許多相同的原則也適用于 UICollectionView,但有些已經(jīng)被修改。

UITableView 最近才開(kāi)始使用類注冊(cè)方法來(lái)創(chuàng)建其單元格。這是集合視圖的唯一方法。

列表視圖的 "批量更新(batch updates)"是通過(guò)調(diào)用一個(gè)方法來(lái)開(kāi)始更新,執(zhí)行更新,然后調(diào)用另一個(gè)方法來(lái)表示更新結(jié)束。然而,集合視圖只提供了基于 Block 塊的 performBatchUpdates: 方法(在我看來(lái)更好)。

這些都是開(kāi)發(fā)人員通過(guò)類來(lái)實(shí)現(xiàn)其目標(biāo)的一些微小的差異。這兩個(gè)類之間更大的哲學(xué)差異是,列表視圖單元格處理了很多內(nèi)部布局。這與集合視圖單元格形成了鮮明的對(duì)比,集合視圖單元格完全不處理。這迫使開(kāi)發(fā)者每次都要從頭開(kāi)始實(shí)現(xiàn)自己的 UITableViewCell 子類。同時(shí),UITableViewCell 有四種不同的 "樣式",定義了它的兩個(gè)文本標(biāo)簽、圖像視圖、"附件 "視圖和編輯樣式的布局方式。相當(dāng)大的區(qū)別!

我相信,如果蘋果今天要引入 UITableView,知道他們?cè)谶^(guò)去 6 年里學(xué)到的框架設(shè)計(jì)知識(shí),UITableViewCell 根本不會(huì)有樣式。相反,他們會(huì)有一些直接的子類,開(kāi)發(fā)者可以使用,也可以實(shí)現(xiàn)自己的子類。

盡管以現(xiàn)代 Objective-C 框架的標(biāo)準(zhǔn)來(lái)看,UITableView 顯得很臃腫,但 UICollectionView 的圓滑在很大程度上要?dú)w功于蘋果公司從最初制作 UITableView 時(shí)學(xué)到的經(jīng)驗(yàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容