创建自定义layout时, 只需要按照前面文章iOS Collection View 编程指导(五)-创建自定义Layout的要求来创建即可, 步骤不多, 比较简单的. 只有一些细节需要注意, 比如layout为cell和view创建attribute顺序, 当collectionView中的item很多时, attribute的重新计算还是缓存技术的应用, 此时应该为特定的item重新计算attribute比较有意义, 当collectionView中的item比较少时, 将所有的item计算完一次后缓存起来可以减少重复计算attribute带来的消耗, 所以此时缓存技术更有意义. 本文会用一个列子来详细讲解具体如何实现自定义布局.
你要牢记, 写出代码不是自定义layout的最终结果, 你需要花时间去设计自定义layout的架构, 使layout拥有更高的性能. 至于如何设计自定义layout, 上一篇文章已经讲解过了.
本文会讲解实现自定义布局的详细步骤, 不过只限于自定义布局的实现, 而不是一个完整的APP的实现, 所以本文不会涉及到相关view和controller的实现.
关于本demo
本demo的内容是展示另一个类的继承图, 如图6-1. 本文提供了demo的关键的代码清单和其解释. 本demo中cell是自定义的, 连接cell的线也是自定义的supplementary视图(为啥不是decoration? 因为cell间的连线是根据内容来的, 和DataSource紧密联系, 所以用supplementary). section0包含一个NSObjectcell, section1包含所有NSObject的子类的cell, section2包含子类的子类, 依次类推. 每个cell中用label来展示类名.
图6-1 类的树形图初始化
第一步继承类UICollectionViewLayout
. 该类提供了实现自定义layout的基础和接口.
对于本demo来说, 使用一个protocol来通知item间的spacing变化. 如果某个单独的item需要从DataSource中获取额外的信息, 你最好也通过delegate来联系DataSource对象, 而不是在自定义layout中直接引用DataSource对象, 这样你的layout的健壮性和可复用性更好. 你的自定义layout不会和特定的DataSource绑定而是可以和任何只要实现协议的对象绑定.
代码清单6-1展示了自定义layout类的头部代码. 所以只要实现MyCustomProtocol
协议的对象都可以利用自定义layout了, layout对象也可以通过该对象来获取额外的信息
代码清单6-1 使用protocol
@interface MyCustomLayout : UICollectionViewLayout
@property (nonatomic, weak) id<MyCustomProtocol> customDataSource;
@end
接下来, 因为collectionView中的item数量比较少, 所以自定义layout使用缓存策略来创建attribute. 在准备布局时, layout会预先为所有的view创建attribute, 然后保存后, 为collectionView请求attribute做了好准备. 代码清单6-2展示了layout的三个私有属性. 其中属性layoutInformation
是一个字典类型, 保存collectionView中所有类型view的attribute; 属性maxNumRows
用来记录collectionView中各列中行数的最大数; 属性insets
控制cell间距和用来设置view的frame, 用来设置content size. 前两个属性在准备布局时设置, 而insets
会在layout对象init时初始化.
代码清单6-2 变量初始化
@interface MyCustomLayout()
@property (nonatomic) NSDictionary *layoutInformation;
@property (nonatomic) NSInteger maxNumRows;
@property (nonatomic) UIEdgeInsets insets;
@end
-(id)init {
if(self = [super init]) {
self.insets = UIEdgeInsetsMake(INSET_TOP, INSET_LEFT, INSET_BOTTOM, INSET_RIGHT);
}
return self;
}
该阶段最后一步是, 创建layout attribute. 尽管该步骤不是必要的, 在本demo中, 因为在计算cell的位置时, 需要访问特定indexpath下的cell的全部子类cell, 这样就能将子类cell和父类cell的frame调整好. 因此需要集成UICollectionViewLayoutAttribute
类, 在自定义attribute子类中用一个数组属性保存cell的子类的attribute, 所以自定义的attribute类的头文件中需要有如下代码:
@property (nonatomic) NSArray *children;
前面讲过, 在自定义Attribute时, 需要重写父类的isEqual:
方法, 具体请看UICollectionViewLayoutAttributes Class Reference
isEqual:
方法的实现比较简单, 因为只需要比较一次(比较cell的子类cell的attribute). 如果子类的Attribute相同那么, cell的Attribute就相同. 具体实现看代码清单6-3.
代码清单6-3 自定义Attribute中的isEqual:
方法实现
-(BOOL)isEqual:(id)object {
MyCustomAttributes *otherAttributes = (MyCustomAttributes *)object;
if ([self.children isEqualToArray:otherAttributes.children]) {
return [super isEqual:object];
}
return NO;
}
到了这里, 为了layout打好了一个地基, 你就可以继续实现自定义layout的主体部分, 请继续往下看.
布局前的准备
collectionView会调用layout的prepareLayout
方法来准备布局. 在本demo中, 使用prepareLayout
来创建所需的所有layout Attribute对象, 然后保存在layoutInformation
字典中, 以供后用. 如果不懂prepareLayout
方法, 请看文档Preparing the Layout, 也可以翻看前面的文章, 里面有讲到. //方法来准备, 本demo, 使用prepareLayout
来创建所需的所有layout attribute对象, 然后保存在layoutInformation
字典中, 以供后用.
创建layout Attribute
本demo中,prepareLayout
的实现包括两个部分. 图6-2展示了实现的第一部分. 遍历所有cell, 如果某个cell是另一个cell的子类, 那么就将只连接起来
代码清单6-4是prepareLayout
方法的第一部分实现. 首先创建两个可变的字典局部变量, layoutInformation
和cellInformation
, layoutInformation
变量是属性layoutInformation
的一个可变副本, 属性时不可变的, 因为在prepareLayout
后, layout Attribute不应该改变. cellInformation
变量用来保存cell的Attribute对象. 然后遍历collectionView的section, 找到section中item, 再遍历item, 创建index path, 使用自定义方法attributesWithChildrenAtIndexPath:
(该方法中会将cell的子类cell的index path保存在Attribute数组中)创建cell的Attribute, 然后将cell的Attribute保存在cellInformation
中, 相应的index path作为key.
代码清单6-4 创建layout Attribute
- (void)prepareLayout {
NSMutableDictionary *layoutInformation = [NSMutableDictionary dictionary];
NSMutableDictionary *cellInformation = [NSMutableDictionary dictionary];
NSIndexPath *indexPath;
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++){
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
MyCustomAttributes *attributes =
[self attributesWithChildrenAtIndexPath:indexPath];
[cellInformation setObject:attributes forKey:indexPath];
}
}
//end of first section
缓存layout Attribute
图6-3,描述了方法prepareLayout
中的步骤2过程. 该过程是从最后一行cell到第一行反过来来创建attribute. 这种方式乍一看很奇怪, 但这是一种消除子cell的frame复杂度的机智的方式. 因为子cell的frame需要和父cell进行匹配, 以及行空间的大小由多少子cell(和子cell的子cell)来决定, 因此在设cell的frame之前, 你需要计算子cell的frame.
在下面步骤1中, 最后一列中的cell按照特定顺序排列. 步骤2中, 开始计算第二列中cell的frame. 在该列中, cell可以顺序排列, 因为没有cell有2个以上的子cell. 然而, 绿色的cell的frame需要适配它的父cell, 所以往下移动用一行. 最后一步中, 第一列的cell开始进行排列, 第二列中的前三个cell是第一列中第一个cell的子cell, 所以第一列中的其他cell往下移动. 在本例中, cell都会被object调整位置来匹配它的父cell, 绿色cell因此匹配到了父cell.
代码清单6-5, 展示了prepareLayout
中另外一部分代码, 改代码开始对item的frame进行计算
代码清单6-5 缓存layout attribute信息
//continuation of prepareLayout implementation
for(NSInteger section = numSections - 1; section >= 0; section—-){
NSInteger numItems = [self.collectionView numberOfItemsInSection:section];
NSInteger totalHeight = 0;
for(NSInteger item = 0; item < numItems; item++){
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
MyCustomAttributes *attributes = [cellInfo objectForKey:indexPath]; // 1
attributes.frame = [self frameForCellAtIndexPath:indexPath
withHeight:totalHeight];
[self adjustFramesOfChildrenAndConnectorsForClassAtIndexPath:indexPath]; // 2
cellInfo[indexPath] = attributes;
totalHeight += [self.customDataSource
numRowsForClassAndChildrenAtIndexPath:indexPath]; // 3
}
if(section == 0){
self.maxNumRows = totalHeight; // 4
}
}
[layoutInformation setObject:cellInformation forKey:@"MyCellKind"]; // 5
self.layoutInformation = layoutInformation
}
下面开始解析上面的代码, 代码中, 方向遍历sections, 从后往前来构建一个继承树. 变量totalHeight
用来记录整颗树的高度. 在计算cell的子cell, 保证两个cell的子cell不会重叠在一起. 代码完成的任务如下:
- 从字典获取数据后创建attribute对象, 此时cell的frame还没创建
- 通过自定义方法
adjustFramesOfChildrenAndConnectorsForClassAtIndexPath:
来遍历所有的cell然后设置cell的frame - 当将attribute放入字典后, 更新
totalHeight
属性, 该属性指明了下一个cell应该从哪里开始叠放. 此时就可以发现自定义protocol的好处了, 遵守该协议的对象需要实现numRowsForClassAndChildrenAtIndexPath:
方法, 在方法中返回某个类有多少子类(行), 也就是一个cell有多个子cell. -
maxNumRows
属性用来确定第一个section的高度(之后在设置content size的时候会用到). 高度最大的列总是第一section. - 最后将所有的cell的attribute对象复制给
layoutInformation
字典
计算Content size
在准备布局前, 代码中的maxNumRows
记录了树的高度. 该信息会用来计算content size. 代码清单6-6展示了CollectionViewContentSize
的实现.
代码清单6-6 计算content size
- (CGSize)collectionViewContentSize {
CGFloat width = self.collectionView.numberOfSections * (ITEM_WIDTH + self.insets.left + self.insets.right);
CGFloat height = self.maxNumRows * (ITEM_HEIGHT + _insets.top + _insets.bottom);
return CGSizeMake(width, height);
}
返回Layout Attribute对象
当所有的Attribute初始化且缓存后, 就要开始准备通过LayoutAttributesForElementsInRect:
方法返回Attribute对象了. 该方法是布局的第二步, 和prepareLayout
方法不一样的是, 该方法是必须实现的. 该方法放回特定区域内item的Attribute对象. 当collection视图中包含太多的item时, collection view会要求该方法创建指定区域内的item的Attribute对象. 本demo中,用的缓存好的Attribute对象. 因此在layoutAttributesForElementsInRect:
中只是简单的循环比例一遍缓存好的Attribute对象, 然后将它们用一个数组装起来后返回给collection view.
单面清单6-7 展示了该方法的实现代码.
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray *myAttributes [NSMutableArray arrayWithCapacity:self.layoutInformation.count];
for(NSString *key in self.layoutInformation){
NSDictionary *attributesDict = [self.layoutInformation objectForKey:key];
for(NSIndexPath *key in attributesDict){
UICollectionViewLayoutAttributes *attributes =
[attributesDict objectForKey:key];
if(CGRectIntersectsRect(rect, attributes.frame)){
[attributes addObject:attributes];
}
}
}
return myAttributes;
}
注意:
layoutAttributesForElementsInRect:
的实现不会影响给定Attribute的view的可见性. 请记住方法中提供的rect区域不一定是可见区域, 方法返回的Attribute对象也不一定是可见view的Attribute对象. 具体见创建自定义Layout-给指定矩形区域内的item提供Layout Attribute
提供特定item的Attribute对象
前面章节有讲过, layout对象必须向collection view提供特定item的attribute. 这些方法会给cell, supplementary view, decoration view提供特定attribute对象, 在比列中, 只使用了cell, 所以这里只需要实现layoutAttributesForItemAtIndexPath:
方法.
代码清单6-8, 展示了该方法的实现.
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
return self.layoutInfo[@"MyCellKind"][indexPath];
}
图6-4, 展示了此时代码运行效果, 所有cell都摆放正确, 子cell和父cell配对完美, 只有cell间的连线没有设置好.
图6-4 代码运行效果
设置supplementary view
目前的代码是已经将所有的cell设置好了, 只差父cell和子cell间的连线没有设置好, 所以类图就会显示不明确. 本demo中使用自定义supplementary view来创建连线. 关于如何设计supplementary view请看前面章节:创建自定义Layout-通过supplementary视图来将突出内容
代码清单6-9, 展示了prepareLayout
方法中关于连线的代码实现, 创建supplementary view的attribute的特点是需要根据一个identifier来确定是何种supplementary view. 因为自定义layout可能包含多种类型的supplementary view.
代码清单6-9 创建supplementary view的attribute对象
// create another dictionary to specifically house the attributes for the supplementary view
NSMutableDictionary *supplementaryInfo = [NSMutableDictionary dictionary];
…
// within the initial pass over the data, create a set of attributes for the supplementary views as well
UICollectionViewLayoutAttributes *supplementaryAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:@"ConnectionViewKind" withIndexPath:indexPath];
[supplementaryInfo setObject: supplementaryAttributes forKey:indexPath];
…
// in the second pass over the data, set the frame for the supplementary views just as you did for the cells
UICollectionViewLayoutAttributes *supplementaryAttributes = [supplementaryInfo objectForKey:indexPath];
supplementaryAttributes.frame = [self frameForSupplementaryViewOfKind:@"ConnectionViewKind" AtIndexPath:indexPath];
[supplementaryInfo setObject:supplementaryAttributes ForKey:indexPath];
...
// before setting the instance version of _layoutInformation, insert the local supplementaryInfo dictionary into the local layoutInformation dictionary
[layoutInformation setObject:supplementaryInfo forKey:@"ConnectionViewKind"];
上面的代码和创建cell的attribute类似, 同样supplementary view的attribute也是利用的缓存机制. 这里使用字典keyConnectionViewKind
来标记supplementary view的attribute.
最后在方法layoutAttributesForSupplementaryElementOfKind:atIndexPath:
中返回特定类型supplementary view的attribute对象, 如代码清单6-10所示
代码清单6-10 返回supplementary view的attribute对象
- (UICollectionViewLayoutAttributes *) layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
return self.layoutInfo[kind][indexPath];
}
总结
通过添加supplementary连线后, 本demo的关键实现已经完成, 接下来你或许要调整layout对象以便节省空间. 在本demo中, 我们展示了一个真实的,基于代码的自定义layout的列子. collection view非常实用, 而且扩展性非常强, 能适用非常多的情况, demo中展示了部分功能, 你可以给collection view添加selecting, moving, move, inserted, deleted等动画效果, 给你的collection view增加色彩. 要想连接如何实现自定义layout的理论知识, 请看前面一章
网友评论