1. 程式人生 > >UICollectionView(純程式碼方式)實現帶上下拉重新整理的瀑布流式

UICollectionView(純程式碼方式)實現帶上下拉重新整理的瀑布流式

瀑布流(WaterFlow)是專案開發過程中的常見佈局,有關於瀑布流(WaterFlow)的實現方式:在UICollectionView未出現之前,瀑布流的實現多半是採用UIScrollView或是UITableView。對於我們這種用慣了表檢視的人來說,UICollectionView倒略顯陌生。有關於UICollectionView的介紹我就不一一贅述,因為一兩句話也很難說清楚。網上有很多優秀的文章專門對其進行了一系列的解說,另有蘋果官方文件可以查閱。本文主要是介紹如何採用純程式碼的方式利用UICollectionView實現帶上下拉重新整理的瀑布流式(WaterFlow)佈局。廢話少說,直接入題。

一、UICollectionView整合上下拉重新整理

(1)簡單的介紹UICollectionView

UICollectionView和UITableView很相似,如果你對錶檢視非常熟悉,上手的速度也會快很多。使用它時需要相應的設定DataSource(UICollectionViewDataSoure)資料來源協議和Delegate(UICollectionViewDelegate)事件協議,並實現相應的協議方法。它與UITableView不同的地方是它有UICollectionViewLayout,可以通過它的子類來定製佈局。系統預設的佈局方式是UICollectionViewDelegateFlowLayout,實現相應的協議(UICollectionViewDelegateFlowLayout)方法,便可達到預設佈局效果。

UICollectionViewCell與UITableViewCell的使用也差不多,一樣的有ReuseIdentifier,可以複用。使用UICollectionViewCell時,需要先註冊,然後再出列使用。至於註冊並出列使用Cell的具體方式,請自行下載文章末尾的BGWaterFlowView或者自行Google。

(2)整合上下拉重新整理方式

一般UITableView新增上下拉重新整理的方式,是把下拉或者上拉重新整理檢視的UI放置UITableView的表頭屬性(tableHeaderView)或者表尾屬性(tableFooterView)。但是,在UICollectionView中沒有這兩個屬性。那麼我們該如何來整合上下拉重新整理呢?這就得說到
UICollectionView佈局中的三種顯示內容檢視:Cells、Supplementary views、Decoration views。(關於這三種檢視的介紹可以去官網查閱相關文件,上面有詳細的解釋)。我們一般是把上下拉重新整理的UI放置在Supplementary views(官方解釋:它能顯示資料但是不同於Cells。不像Cell,Supplementary views是不能被使用者選定的。相反,你可以用它去實現類似於給一個指定的section或者整個CollectionView新增頁首和頁尾檢視這樣的功能)上面,其實,Supplementary views就和TableView的section頭檢視和尾檢視差不多。但是用法卻大不相同。在自定義瀑布流佈局中一定要把它也計算進去,不然顯示就會有異常。

  • 註冊頭檢視和尾檢視單元格型別
//註冊頭檢視:kind代表Supplementary檢視型別,UICollectionElementKindSectionHeader表示組頭

[self registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"bGCollectionHeaderView"];

//註冊尾檢視:UICollectionElementKindSectionHeader表示組尾

    [self registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"bGCollectionFooterView"];
  • 實現自定義組檢視viewForSupplementaryElementOfKind代理方法
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {

    //kind代表Supplementary檢視型別:頭或尾檢視(header OR footer)

    if([kind isEqual:UICollectionElementKindSectionHeader]) {
        UICollectionReusableView *collectionHeaderView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"bGCollectionHeaderView" forIndexPath:indexPath];

        //初始化下拉重新整理或者自定義頁首檢視UI介面
        ...

        return collectionHeaderView;
    } else if([kind isEqual:UICollectionElementKindSectionFooter]) {
        UICollectionReusableView *collectionFooterView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"bGCollectionFooterView" forIndexPath:indexPath];

        //初始化上拉重新整理或者自定義頁尾檢視UI介面
        ...

        return collectionFooterView;
    }

    return nil;
}

完成以上兩步,整合上下拉重新整理的功能總算完成了。接下來你可以執行一下工程,看結果是否符合預期。如果你CollectionView使用的是系統的UICollectionViewFlowLayout佈局,一定要返回組頭或者組尾的高度,否則就會出現組頭或者組尾無法顯示(本文的下拉重新整理檢視所在的組頭不返回高度不受影響,是因為它的縱座標(y值)本來就是負數)。另外,還有一點需要注意的是,如果你的CollectionView中存在多組,最好是把上下拉重新整理所在的組頭或者組尾與其他組(section)的組頭或組尾分開,相應的需要多註冊一個組頭或者組尾檢視,分情況進行判斷(這部分如果還有問題,可以在文章末尾留言,我會及時回覆)。

二、自定義瀑布流(WaterFlow)式佈局

(1)簡單的介紹UICollectionView佈局形式

一般來講,UICollectionView的佈局形式分為兩種:

  • 佈局與內容獨立,其佈局不需要根據顯示的單元格內容來計算。Cell的顯示順序與內容順序一致。Cell排滿一行則換至下一行繼續排列。系統的UICollectionViewFlowLayout就是這樣。

  • 佈局需要計算內容,本文的瀑布流Demo區域性正是如此。UICollectionViewFlowLayout系統佈局在換行的時候,由於每個item高度不同,從而導致佈局錯亂,無法滿足你的需求。所以,這個時候你需要計算每個item的高度,最終用來確定item顯示的位置。

從上述內容我們可以看出,一般如果佈局需要計算內容的時候,我們就不應該直接使用UICollectionViewFlowLayout佈局了,而是應該子類化一個UICollectionViewLayout,從而達到私人定製的目的。

(2)建立自定義佈局

  1. 使用”prepareLayout”方法去執行一些CollectionView所需要的佈局資訊的預先計算操作。
  2. 使用”collectionViewContentSize”方法去返回根據你最初計算的整個內容區域的總體大小。
  3. 使用”layoutAttributesForElementsInRect:”方法來返回指定區域的單元格與檢視屬性。

下面我們通過Demo來詳細的講解一下如何自定義瀑布流,廢話不多說,直接上程式碼(本文采用的Demo為BGWaterFlowView,GitHub傳送門https://github.com/yangshebing/BGWaterFlowView‘>請點選這裡):

  • 重寫”prepareLayout”方法計算佈局資訊。
- (void)prepareLayout{
    [super prepareLayout];
    ...

    //計算每個顯示項的寬度,horizontalItemSpacing代表項與項之間的水平間隔, 
    columnNum代表總列數,contentInset代表上下左右的內填充。詳見下圖說明

    self.itemWidth = (self.collectionView.frame.size.width - (self.horizontalItemSpacing * (self.columnNum - 1)) - self.contentInset.left - self.contentInset.right) / self.columnNum;

    //如有下拉重新整理需求的則需要提供頭檢視(Supplementary view)佈局屬性。
    demo中的headerHeight屬性可設定是否顯示頭檢視(是否計算頭檢視的佈局屬性)。

    if(self.headerHeight > 0){

    //通過layoutAttributesForSupplementaryViewOfKind:withIndexPath:方法來建立Supplementary檢視佈局屬性物件,kind用來區分Supplementary檢視型別(header或者footer)。

        self.headerLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];

        //修改佈局引數frame屬性

        self.headerLayoutAttributes.frame = CGRectMake(0, 0, self.collectionView.frame.size.width, self.headerHeight);
    }

    //初始化儲存佈局屬性的字典
    NSMutableDictionary *cellLayoutInfoDic = [NSMutableDictionary dictionary];
    //初始化列高陣列
    NSMutableArray *columnInfoArray = [self columnInfoArray];
    NSInteger numSections = [self.collectionView numberOfSections];
    for(NSInteger section = 0; section < numSections; section++)  {
        NSInteger numItems = [self.collectionView numberOfItemsInSection:section];
        for(NSInteger item = 0; item < numItems; item++){
            //獲取列高最小的model,以它的高作為y座標
            BGWaterFlowModel *firstModel = columnInfoArray.firstObject;
            CGFloat y = firstModel.height;
            CGFloat x = self.contentInset.left + (self.horizontalItemSpacing + self.itemWidth) * firstModel.column;

            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];

            //通過代理方法傳入對應item的高度。

            CGFloat itemHeight = [((id<BGWaterFlowLayoutDelegate>)self.collectionView.delegate) collectionView:self.collectionView layout:self heightForItemAtIndexPath:indexPath];

            //通過layoutAttributesForCellWithIndexPath:方法來建立cell的佈局屬性物件。

            UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            //計算item的佈局屬性
            itemAttributes.frame = CGRectMake(x, y+self.contentInset.top+self.headerHeight, self.itemWidth, itemHeight);
            //計算當前列高,verticalItemSpacing代表項與項之間的垂直間隔。
            firstModel.height += (itemHeight + self.verticalItemSpacing);
            //儲存新的列高,並進行排序:從後往前查詢,查詢到高度比它小的物件,就插入到該物件之後。
            [self sortArrayByHeight:columnInfoArray];

     //儲存計算好的item佈局屬性
     cellLayoutInfoDic[indexPath] = itemAttributes;
        }
    }

    //儲存區域性佈局屬性字典到全域性字典中
    self.cellLayoutInfoDic = [cellLayoutInfoDic copy];

    //按照前面的排序邏輯,列高陣列中的最後一個元素,就是高度最大的一列。
    BGWaterFlowModel *lastModel = columnInfoArray.lastObject;

    //如有上拉重新整理需求的則需要提供尾檢視佈局屬性。
    demo中的footerHeight屬性可設定是否顯示尾檢視(是否計算尾檢視的佈局屬性)。

    if(self.footerHeight > 0){
        self.footerLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
        //計算尾檢視的佈局屬性
        self.footerLayoutAttributes.frame = CGRectMake(0, lastModel.height+self.headerHeight+self.contentInset.top+self.contentInset.bottom, self.collectionView.frame.size.width, self.footerHeight);
    }

    //直接計算出collectionView的contentSize
    self.contentSize = CGSizeMake(self.collectionView.frame.size.width, lastModel.height+self.headerHeight+self.contentInset.top+self.contentInset.bottom+self.footerHeight);
}

好吧,這麼一大坨程式碼看到就頭疼了,來張圖幫助理解一下!圖糙理不糙,理解萬歲

這樣看上去是不是更清晰了?

  • 重寫”collectionViewContentSize”方法返回collectionView的內容高度。

- (CGSize)collectionViewContentSize{
    //返回計算好的collectionView內容高度
    return self.contentSize;
}
  • 重寫”layoutAttributesForElementsInRect”方法返回指定矩形區域中的cell或者其他型別檢視佈局屬性。
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    //返回一組已經計算好的佈局屬性。

    NSMutableArray *attributesArrs = [NSMutableArray array];
    [self.cellLayoutInfoDic enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath,
                                                                UICollectionViewLayoutAttributes *attributes,
                                                                BOOL *stop) { 
        //遍歷佈局屬性儲存字典,添加布局屬性至陣列中                                                       
        if (CGRectIntersectsRect(rect, attributes.frame)) {
            [attributesArrs addObject:attributes];
        }
    }];

    //如果有頭檢視或者尾檢視則需要新增頭檢視與尾檢視的佈局屬性

    if (self.headerLayoutAttributes && CGRectIntersectsRect(rect, self.headerLayoutAttributes.frame)) {
        [attributesArrs addObject:self.headerLayoutAttributes];
    }


    if (self.footerLayoutAttributes && CGRectIntersectsRect(rect, self.footerLayoutAttributes.frame)) {
        [attributesArrs addObject:self.footerLayoutAttributes];
    }

    return attributesArrs;
}

結語:

https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionViewLayout_class/index.html#//apple_ref/occ/cl/UICollectionViewLayout“>UICollectionViewLayout相關的內容姿勢非常多,相關文章也非常多。本文介紹的瀑布流佈局也只是其中的冰山一角,很多東西官方文件中已經介紹的非常詳細了。所以,還是建議多看官方文件。本文主要注重專案的實用性,在後面就直接上程式碼了,可能在理論方面還是有所欠缺,對於不足的地方,希望大家多提建議,一起交流學習,一起進步。如果文中有看不懂的地方,還請您留言。我會及時進行回覆。最後,本文的Demo就在於方便徒手擼程式碼的人使用。你懂的。後期會不斷的更新優化,進一步的完善。敬請期待…

Demo執行瀑布流效果截圖如下所示:

參考部落格地址:

轉載請註明出處