美文网首页
iOS Collection View 编程指导(六)-一个自定

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

作者: 陵无山 | 来源:发表于2018-10-09 19:59 被阅读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的理论知识, 请看前面一章

    相关文章

      网友评论

          本文标题:iOS Collection View 编程指导(六)-一个自定

          本文链接:https://www.haomeiwen.com/subject/wfiyaftx.html