iOS Collection View 编程指导(六)-一个自定

2018-10-09  本文已影响39人  陵无山

创建自定义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-2 将子类和父类联系起来

代码清单6-4是prepareLayout方法的第一部分实现. 首先创建两个可变的字典局部变量, layoutInformationcellInformation, 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-3 计算cell的frame的过程

代码清单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不会重叠在一起. 代码完成的任务如下:

  1. 从字典获取数据后创建attribute对象, 此时cell的frame还没创建
  2. 通过自定义方法adjustFramesOfChildrenAndConnectorsForClassAtIndexPath:来遍历所有的cell然后设置cell的frame
  3. 当将attribute放入字典后, 更新totalHeight属性, 该属性指明了下一个cell应该从哪里开始叠放. 此时就可以发现自定义protocol的好处了, 遵守该协议的对象需要实现numRowsForClassAndChildrenAtIndexPath:方法, 在方法中返回某个类有多少子类(行), 也就是一个cell有多个子cell.
  4. maxNumRows属性用来确定第一个section的高度(之后在设置content size的时候会用到). 高度最大的列总是第一section.
  5. 最后将所有的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的理论知识, 请看前面一章

上一篇 下一篇

猜你喜欢

热点阅读