UICollectionView: 糊一張裝飾檢視 Decoration View 的一點經驗
重點:
一, 裝飾檢視 Decoration View ,蘋果的例子是一個 cell 貼一張背景圖。
實際上,一個 section ,貼一張背景圖,可以的。
蘋果設計的非常靈活,基本上背景圖想怎麼糊上去,就怎麼糊
實踐中發現
二, 設定 Decoration View ,手寫 UICollectionViewFlowLayout ( 或 UICollectionViewLayout ),是寫死的。
佈局顯示,一般有一個網路請求。資料請求回來前,走自定義的 layout , 到具體的 indexpath, 訪問手工設定有,因實際不存在,崩。
因為沒有網路請求回資料,實際的 section 數量一般為 0.
需判斷一下。
三, 無關 Decoration View 。
做了一個商品首頁的需求,UICollectionView 七層樓,每層樓都不一定有,樓層順序也不一定。
如果寫 if else ,就要命。通過字典配置,解決
詳細介紹:
第一點,一個 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{
// 最後一層樓,固定的情況, 簡單點, 還是用 if 了
if( 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 大佬寫過的一篇部落格
參考資料: