重点:
一, 装饰视图 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

第一点,一个 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 大佬写过的一篇博客
参考资料: