UICollectionView: 糊一張裝飾檢視 Decoration View 的一點經驗

鄧輕舟發表於2019-01-06

重點:

一, 裝飾檢視 Decoration View ,蘋果的例子是一個 cell 貼一張背景圖。

實際上,一個 section ,貼一張背景圖,可以的。

蘋果設計的非常靈活,基本上背景圖想怎麼糊上去,就怎麼糊

實踐中發現

二, 設定 Decoration View ,手寫 UICollectionViewFlowLayout ( 或 UICollectionViewLayout ),是寫死的。

佈局顯示,一般有一個網路請求。資料請求回來前,走自定義的 layout , 到具體的 indexpath, 訪問手工設定有,因實際不存在,崩。

因為沒有網路請求回資料,實際的 section 數量一般為 0.

需判斷一下。

三, 無關 Decoration View 。

做了一個商品首頁的需求,UICollectionView 七層樓,每層樓都不一定有,樓層順序也不一定。

如果寫 if else ,就要命。通過字典配置,解決


詳細介紹:

配圖說明:

“第一個 cell" , 是第一個 section, 只有 1 個 item

“第二個 cell" , 是第二個 section, 有 5 個 item

UICollectionView: 糊一張裝飾檢視 Decoration View 的一點經驗

第一點,一個 section ,貼一張背景圖

設定背景圖的區域,糊上去,end

具體烹飪教程如下:

裝飾檢視是 UICollectionViewLayout 的功能,不是 UICollectionView 的。

UICollectionView 的方法、代理方法 (delegate, datasource)都不涉及裝飾檢視。

UICollectionView 對裝飾檢視一無所知,UICollectionView 按照 UICollectionViewLayout 設定的渲染。

要用裝飾檢視,就要自定製 UICollectionViewLayout,也就是 UICollectionViewLayout 的子類。這個 UICollectionViewLayout 子類,可以新增屬性、代理屬性,通過設定代理協議方法,來自定製裝飾檢視。

本文 Demo 舉的例子是新增一個裝飾檢視背景圖片。

(沒有涉及使用代理,設定協議方法,進一步控制裝飾檢視)

簡要說來,自定製的 layout 子類,實現一個裝飾檢視,五步:

步驟 1,

要有 Decoration View 檔案。

先建立一個 UICollectionResuableView 的子類, 這個就是具體的裝飾檢視

@interface FrontDecorationReusableView()
// 裝飾檢視,裡面就一張圖片
@property (nonatomic, strong) UIImageView * imageView;

@end

@implementation FrontDecorationReusableView
- (instancetype)initWithFrame:(CGRect)frame{

    if (self = [super initWithFrame:frame]){ 
        self.backgroundColor = UIColor.whiteColor;
        _imageView = [[UIImageView alloc] init];
        [self addSubview: _imageView];
        // 使用了 masonry 佈局
        [_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.edges.mas_equalTo(self);
        }];
    }
    return self;
}
複製程式碼

步驟 2,

layout 中註冊裝飾檢視。

有了裝飾檢視,組裝在一起 (wire it up)

自定製的 layout 子類中,註冊 UICollectionResuableView 的子類,也就是裝飾檢視。

呼叫 - (void)registerClass:(nullable Class)viewClass forDecorationViewOfKind:(NSString *)elementKind; 方法。

一般在 - (void)prepareLayout 方法中註冊。

- (void)prepareLayout {
    [super prepareLayout];
    [self registerClass: FrontDecorationReusableView.class forDecorationViewOfKind: FDRFrontDecorationReusableView];
}
複製程式碼

步驟 3,

設定裝飾檢視的位置。

- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath 方法,設定裝飾檢視 UICollectionResuableView 的位置,因為該方法返回了裝飾檢視的佈局屬性。

+ (instancetype)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind withIndexPath:(NSIndexPath *)indexPath; 方法,構建佈局屬性,並作相關的配置。

先設定裝飾檢視的具體位置,

- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath{
    
    if (elementKind == FDRFrontDecorationReusableView && indexPath.section == 1) {
        DecorationLayoutAttributes * attributes = [DecorationLayoutAttributes layoutAttributesForDecorationViewOfKind: FDRFrontDecorationReusableView withIndexPath: indexPath];
        // 通過屬性,外部設定裝飾檢視的實際圖片 ( 後有介紹 )
        attributes.imgUrlStr = self.imgUrlString;
       // 這裡,裝飾檢視的位置是固定的
        CGFloat heightOffset = 16;
        attributes.frame = CGRectMake(0, KScreenWidth * 0.5 - heightOffset, KScreenWidth, 102 + heightOffset);
        attributes.zIndex -= 1;
        return attributes;
    }
    return nil;
}
複製程式碼

步驟 4,

重寫 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect 方法, 該方法會返回給定區域內,所有檢視 ( 格子檢視、補充檢視(header \ footer)、裝飾檢視 ) 的佈局屬性。

這裡要糊上裝飾檢視,layoutAttributesForElementsInRect:返回的佈局屬性陣列,需含有呼叫 - (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath 方法中設定的佈局屬性。

這一步比較關鍵,collectionView 得到了足夠的資訊,顯示裝飾檢視。 當 collectionView 呼叫 layoutAttributesForElementsInRect:,他會提供每一種裝飾檢視的佈局屬性。 collectionView 對裝飾檢視是隔離的,一無所知。看到的 collectionView 的裝飾檢視,是自定製 layout 提供的。

步驟 2中,註冊了裝飾檢視,即建立了自定製的裝飾檢視例項。collectionView 會根據佈局屬性,放置好。

把上一步設定的裝飾檢視佈局屬性,交給 collectionView 使用

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    NSArray<UICollectionViewLayoutAttributes *> * rawArr = [super layoutAttributesForElementsInRect: rect];
    NSMutableArray<UICollectionViewLayoutAttributes *> * array = [[NSMutableArray alloc] initWithArray: rawArr];
// 避免崩潰 ( 後有介紹 )
    NSInteger numberOfSections = [self.collectionView numberOfSections];
    if (numberOfSections == 0) {
        return rawArr;
    }
    UICollectionViewLayoutAttributes * decorationAttrs = [self layoutAttributesForDecorationViewOfKind: FDRFrontDecorationReusableView atIndexPath: [NSIndexPath indexPathForItem: 0 inSection: 1 ]];
    if (decorationAttrs && CGRectIntersectsRect(rect, decorationAttrs.frame)) {
        [array addObject: decorationAttrs];
    }
    return [array copy];
}


複製程式碼

步驟 5,

怎麼給裝飾檢視傳值?

三步走:

CollcetionView -> layout -> layoutAttributes -> decorationView 裝飾檢視

本文 demo ,是配置具體的裝飾圖片。

先給自定製的 layout 一個圖片地址屬性,

@interface DecorationFlowLayout : UICollectionViewFlowLayout
@property (nonatomic, copy) NSString * imgUrlString;
@end
複製程式碼

然後想辦法傳過去,就好了

collectionView 設定 layout 的圖片 url ,間接控制裝飾檢視的圖片 url

......
self.decorationFlowLayout.imgUrlString = @"https://fscdn.zto.com/GetPublicFile/ztPK4Y-WGgWKiRNfkygd3oYQ/thumbnail_747d31f481044bf6a149c7483cd097a5.jpg";
    [self.newMainCollectionView reloadData];
}
複製程式碼

自定製 layout 與裝飾檢視也是隔離的。建立自定製佈局屬性物件 UICollectionViewLayoutAttributes 來傳值,相當於找了一個信使。

使用 UICollectionViewLayoutAttributes 的子類,新增屬性傳值。

@interface DecorationLayoutAttributes: UICollectionViewLayoutAttributes

@property (nonatomic, copy) NSString * imgUrlStr;
@end

複製程式碼

layoutAttributesForDecorationViewOfKind: 中配置, 上有提及,

DecorationLayoutAttributes * attributes = [DecorationLayoutAttributes layoutAttributesForDecorationViewOfKind: FDRFrontDecorationReusableView withIndexPath: indexPath];
        // 通過屬性,外部設定裝飾檢視的實際圖片
        attributes.imgUrlStr = self.imgUrlString;
複製程式碼

最後一小步,

把自定製 LayoutAttributes 的圖片 url 傳遞給裝飾檢視, 靠 - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes 方法。

當 collectionView 配置裝飾檢視的時候,會呼叫該方法。layoutAttributes 作為引數,取出 imgUrlStr 屬性使用,就可以了

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes{
    if ( [layoutAttributes valueForKey: @"imgUrlStr"] && [layoutAttributes isMemberOfClass: NSClassFromString(@"DecorationLayoutAttributes")] ) {
        [self.imageView sd_setImageWithURL_str: [layoutAttributes valueForKey: @"imgUrlStr"]];
    }
}

複製程式碼

第二點,怎麼處理,看了一下大神寫的 CHTCollectionViewWaterfallLayout

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    NSArray<UICollectionViewLayoutAttributes *> * rawArr = [super layoutAttributesForElementsInRect: rect];
    NSMutableArray<UICollectionViewLayoutAttributes *> * array = [[NSMutableArray alloc] initWithArray: rawArr];
    NSInteger numberOfSections = [self.collectionView numberOfSections];
//    if (numberOfSections == 0) {
//        return rawArr;
//    }
UICollectionViewLayoutAttributes * decorationAttrs = [self layoutAttributesForDecorationViewOfKind: FDRFrontDecorationReusableView atIndexPath: [NSIndexPath indexPathForItem: 0 inSection: 1 ]];

// 因為這一行,崩
// 資料請求回來前,不存在實際的區間。 indexPath 也沒有。
複製程式碼

2019-01-05 16:54:59.230718+0800 Improved[31532:238435] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'request for layout attributes for decoration view of kind FrontDecorationReusableView in section 1 when there are only 0 sections in the collection view'

datasource 資料來源沒設定,就先返回

判斷一下情況

 if (numberOfSections == 0) {
        return rawArr;
    }
複製程式碼

第三點,

if 直接判斷條件,有一個隨機的語義。

字典就是雜湊表,知道鍵,直接取值。也有一個隨機的語義。

正適合這種隨機配置的情況。

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
// 最後一層樓,固定的情況, 簡單點, 還是用 ifif( indexPath.section == self.floorDataLists.count ){
        HotSalesCell * hotSaleCollectionViewCell = [collectionView dequeueReusableCellWithReuseIdentifier: kHotSaleCollectionViewCell forIndexPath: indexPath];
        MyProduct * myProduct = self.hotSaleProducts[indexPath.row];
       hotSaleCollectionViewCell.hotSalesProduct = myProduct;
       return hotSaleCollectionViewCell;
   }
   UICollectionViewCell * cell = nil;
   
   FloorDataList * floorDataList = self.floorDataLists[indexPath.section];
   NSString * keyStr = floorDataList.floorTypeName;
// 先找出,關鍵的樓層配置資訊, 作為鍵
   NSNumber * newSectionIndex = self.mapOne_sequence[keyStr];
// 先使用字典,化無序的配置,為有限的集合情況
// 使用 switch ... case  , 對每一種情況,針對性處理就好了
   FloorDataModel * floorDataModel = floorDataList.floorData[indexPath.item];
   switch (newSectionIndex.unsignedIntegerValue){
       case 1:
       {
           BannerReusableView * bannerReusableView = [collectionView dequeueReusableCellWithReuseIdentifier:  kBannerReusableView forIndexPath: indexPath];
               NSMutableArray * imgLinksArray = [NSMutableArray array];
               for (FloorDataModel * floorDataModel in floorDataList.floorData){
                   [imgLinksArray addObject: floorDataModel.uploadImage];
               }
               [bannerReusableView parseBannerPics: imgLinksArray  andSection: indexPath.section];
           }
           / / Cell A 
           cell = bannerReusableView;
       }
           break;
       case 2:
           {
               cell = // ... Cell B; 
           }
           break;
       case 3:
           {
                cell = // ... Cell C; 
           }
           break;
       case 4:
       {
            cell = // ... Cell D; 
       }
           break;
       default:
           break;
   }
   return cell;
}


- (NSDictionary *)mapOne_sequence{
   if (!_mapOne_sequence) {
       _mapOne_sequence = @{kFloorTypeNameFiveIcon: @(2),
                   kFloorTypeNameBanner: @(1),
                   kTenGoods: @(3),
                   kAcrossColumn: @(4)
                   };
   }
   return _mapOne_sequence;
}


複製程式碼

先找出,關鍵的樓層配置資訊, 作為鍵

使用字典,化無序的配置,為有限的集合情況

使用 switch ... case , 對每一種情況,針對性處理就好了

(暫未想出更好的辦法)

更多見 Demo 程式碼:

dev.tencent.com/u/dengjiang…

參考了下 casa 大佬寫過的一篇部落格


參考資料:

stackoverflow.com/questions/1…

相關文章