iOS技术分享iOS 细节大集合iOS开发必修课

CollectionView详解

2016-09-07  本文已影响4686人  varlarzh

最近将 UICollectionView 进行了一个全面的学习及总结,参考了网上大量的文章,把官方文档进行了大概翻译,最后有个小Demo。

UICollectionView基础知识

collection view 是运用一个灵活多变的布局呈现一系列有序数据项的一种方法。collection view最通常的使用使用像网格状排列来呈现数据项,但是 iOS 的 collection view 的能力不仅限于行和列。使用 collection views, 视觉元素的精确布局可通过子类化定义并被动态改变。所以你可以实现网格,栈,圆形布局,动态改变布局,或任何你可以想象的排列布局。

Collection View 是由多个对象协作而成

UICollectionView

在上面可以看出,一个 CollectionView 视图从 data source 中获取数据信息,data source 和 delegate 来管理具体的单元对象,包括选中、未选中、高亮、未高亮等状态。Layout 决定了每个 cell 之间的边界及布局信息,并通过 layoutattributes 来决定每个 cell 的属性。

cv_objects

ReusableView 采用的可重用 cell 的方法,可以提高效率,支持三种不同的可重用的view

Layout 对象控制视觉显示

layout object 决定着 cell 的大小,位置还有其他显示相关的属性。
layout object 没有接触任何视图,它只是一个属性集合。而没有任何限制,你可以随你所想。
layout object 不止可以控制视图的宽高尺寸,还可以控制视图之间的关系属性,透明度,3D,和他跟其他视图之间的可见性对比。

Collection Views Initiate Animations Automatically

collection views 在 cell 移动、插入或删除的时候,会有默认的动画。可以自定义这些动画。

设计 Data Source 和 Delegate

每个 collection view 必须有一个 data source 的对象,是 app 展示的内容。必须提供 collection view 需要的信息,比如多少个 items 等。delegate 对象一般用于和内容的交互相关的。可以管理 cell 的高亮选中状态,除此之外还有额外的工作,比如自定义布局的 cell 的大小及间隔 spacing 等。

用 Data Source 管理你的内容

data source 需要遵循 UICollectionViewDataSource 协议提供三个信息:

几个方法

设计你的 Data Objects

object

一个有效的数据源使用节和元素来组织基础的数据对象。一个简单的解决方案就是如图一样,把数据源组织成一个嵌套数组,最外层数据包含一个或多个数据展示数据源中的节,每个节数据包含多个元素。当设计数据结构时,要考虑性能问题,集合视图访问数据源仅是为了计算一共多少元素并获取当前屏幕所需的数据对象。如果布局对象只是依赖于数据对象,当数据对象包含上千条数据的时候,性能会受到大幅的影响。

告诉集合视图相关的数据源信息

当以下发生时,需要提供给集合视图相关 data source

collectionView.performBatchUpdates({ () -> Void in
            collectionView.insertItemsAtIndexPaths(insertIndexPaths)
            collectionView.moveItemAtIndexPath(currentIndexPath, toIndexPath: toIndexPath)
            }, completion: { (isFinish) -> Void in
        })

通过numberOfSectionsInCollectionView:来提供分组数量,可选的方法,默认是1,使用collectionView:numberOfItemsInSection:来提供每个分组的 items 的数量。

配置单元格(cells)和增补视图(supplementa views)

要给单元提供内容,需要做的两件事:

复用标识(Reuse identifiers)可以让注册多种类型的单元和增补视图成为可能。当请求一个视图或一个对象时,可以使用提供的索引路径决定想要使用哪种类型的视图和单元格,然后传递适当的重用标识给 dequeue 函数。

注册单元格和增补视图

两种方式注册单元格,在 storyboard 中注册或者手动代码注册。

supplementary views 除了重用标识外,还有一个额外的标识,每个 layout 对象负责定义 supplementary view 支持的 kind 。比如 UICollectionViewFlowLayout类支持两种类型 UICollectionElementKindSectionHeaderUICollectionElementKindSectionFooter

注意:如果使用的自定义的 layouts,需要自己定义 supplementary views 的类型。

重用和配置单元格和增补视图

data source 对象负责提供 cell 和 supplementary view 的内容,包含两个方法: collectionView:cellForItemAtIndexPath:collectionView:viewForSupplementaryElementOfKind:atIndexPath:,实现两个方法的简单步骤:

例子:

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let newCell = collectionView.dequeueReusableCellWithReuseIdentifier(MyCellID, forIndexPath: indexPath) as! MyCustomCell
         newCell.cellLabel.text = "Section:\(indexPath.section), Item:\(indexPath.item)"
        return newCell
    }

插入、删除、移动 sections 和 items

插入、删除,移动单个 section 或者 items,必须遵循两个步骤:

当插入、删除和移动单个 item 的时候,collection view 的方法会对这些变化自动产生一个动画效果。如果想多个改变共用一个动画,必须在performBatchUpdates:completion:方法的闭包里执行插入、删除、移动等。

例子:

 collectionView.performBatchUpdates({ 
            let itemPaths = collectionView.indexPathsForSelectedItems()
            //从 data source 中删除数据
            deleteItemFromDataSourceAtIndexPaths()
            collectionView.deleteItemsAtIndexPaths(itemPaths)
            }, completion: nil)

管理 cell 的选中、高亮状态

collection view 支持单个 item 选中,也可以配置为多个 item 选中或者禁止选中。当 selectedBackgroundView 中包含一个有效的 view 的时候,当 cell 是高亮或选中状态时会显示这个 view。此种方式可以改变 cell 高亮或选中时的背景颜色。

collection views 的代理提供的一些方法:

改变 cell 背景颜色的一个例子

- (void)collectionView:(UICollectionView *)colView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell* cell = [colView cellForItemAtIndexPath:indexPath];
    cell.contentView.backgroundColor = [UIColor blueColor];
}
 
- (void)collectionView:(UICollectionView *)colView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell* cell = [colView cellForItemAtIndexPath:indexPath];
    cell.contentView.backgroundColor = nil;
}

高亮和选中区别

cell_selection_semantics

显示编辑按钮

当在 cell 中执行一个长按的手势,collection view 可以显示一个编辑按钮,可以用于剪切、粘贴、复制这个 cell。编辑按钮能被显示必须满足以下条件:

layouts 布局之间的转场

最简单的在两个布局之间转变的方法是 setCollectionViewLayout:animated:。如果需要控制转场动画或者制作可交互的转场动画,需要使用 UICollectionViewTransitionLayout 对象。

使用 UICollectionViewTransitionLayout 对象的步骤:

使用流水布局 Flow Layout

配置 flow layout 的几个步骤:

自定义属性

指定 item 的大小

对于所有的 items 可以使用属性 itemSize 来设置大小。如果设定不同的 itemSize,需要实现 collectionView:layout:sizeForItemAtIndexPath: 方法

指定 items 和 lines 之间的间隔

注意此处设置的是最小间隔,实际间隔可能大小此间隔。可以通过属性设置 minimumLineSpacingminimumInteritemSpacing 或者使用代理方法来实现 collectionView:layout:minimumLineSpacingForSectionAtIndex:collectionView:layout:minimumInteritemSpacingForSectionAtIndex:

使用内边距设置边缘

flow_section_insets

使用 Flow Layout 的子类

可以自己生成一个 flowlayout 的子类,然后更改其中的属性方法等实现自定义。

生成自定义的布局 Layout

实现自定义的布局不难,最难的如何在布局中计算 items 位置。

实现 UICollectionViewLayout 子类

继承 UICollectionViewLayout 需要完成的重要的内容:

理解 Layout 的布局过程

invalidateLayout 会使当前的 layout 无效,并触发 layout 的更新,会强迫 layout 对象重新计算 layout 属性。与 reloadData 不一样,当 data source 中的数据发生改变,适合用 reloadData 方法。

在布局过程中,会按顺序调用一下函数,可以在这些方法中计算 item 的位置信息。

prepareLayout是专门用来准备布局的,在prepareLayout方法里面我们可以事先就计算后面要用到的布局信息并存储起来,防止后面方法多次计算,提高性能。例如,我们可以在此方法就计算好每个 cell 的属性、整个 CollectionView 的内容尺寸等等。此方法在布局之前会调用一次,之后只有在调用invalidateLayoutshouldInvalidateLayoutForBoundsChange:返回YES和 UICollectionView 刷新的时候才会调用。

cv_layout_process

prepareLayout方法是为确定布局中各 cell 和 view 位置做计算,需要在此方法中算出足够的信息以供后续方法计算内容区域的整体 size,collection view 使用 content size 以正确地配置 scroll view。比如 content size 长宽均超过屏幕的话,水平与竖直方向的滚动都会被 enable。基于当前滚动位置,collection view 会调用 layoutAttributesForElementsInRect:方法以请求特定 rect (有可能是也可能不是可见 rect)中 cell 和 view 的属性。到此,core layout process 已经结束了。

layout 结束之后,cells 和views 的属性在你或者 collection view invalidate布局之前都不会变。调用 invalidateLayout 方法会导致新的一次 layout process 开始,以调用 prepareLayout 方法开始。collection view 可以在滚动的过程中自动 invalidate 布局,用户滚动内容过程中,collection view调用layout 的 shouldInvalidateLayoutForBoundsChange: 方法,如果返回值为 YES 则 invalidate 布局。(但需要知道的是,invalidateLayout 并不会马上触发 layout update process,而是在下一个view更新周期中,collection view 发现 layout 已经 dirty 才会去更新)

创建布局属性 Layout Attributes

自定义 layout 需要返回一个 UICollectionViewLayoutAttributes 类的对象,这些对象可以在很多不同的方法中创建,但创建时间可以根据具体情况决定。如果 collection view 不会处理上千个 items 时,则 prepareLayout 创建会比用户滚动过程中用到时在计算更高效,因为创建的属性可以缓存起来。如果计算所有属性并缓存起来所带来的性能消耗比请求时在计算属性的消耗更大,则可以在请求的时候在计算相关属性。

UICollectionViewLayoutAttributes的属性:

创建 UICollectionViewLayoutAttributes 类对象时,可以使用一些方法:

view 的类型不同,必须使用正确的类方法,因为 collection view 会根据这些信息向 data source 对象请求适当类型的 view,使用错误的方法在错误的地方创建错误的 view。

创建每个属性对象后,要将相应的 view 的相关属性设置上。最基本的要设置 view 的 size 和 position 信息。如果布局中有 view 重叠了,需要配置正确的 zIndex 属性来维持有序的状态。其他属性可以控制 cell 或 view 的外观及可见性。

准备 Layout

在一个布局周期中,首先会调用 prepareLayout 方法,可以来执行一些准备工作,可以进行一些 layout 布局需要的计算等,可以存储一些 layout attributes 信息。

给定矩形中的 items 布局属性

layout process 的最后,collection view 会调用 layoutAttributesForElementsInRect: 方法,对于一个大的可滚动内容区域,collection view 可能只会请求当前可见的那部分区域中的所有 items 属性。这个方法支持获取任意 rect 中 items 的信息,因为有可能在插入及删除时做动画效果。

cv_visible_elements

layoutAttributesForElementsInRect: 方法实现需要尊重如下的步骤:

不仅要记住缓存layout信息能够带来性能提升,也要记住不断重复为cells创建新layout属性的计算代价是十分昂贵的,足以影响到app的性能。当collection view管理的items量很大时,采用在请求时创建layout属性的方式是十分合理的。

按需提供布局属性

collection view 会在正常的 layout 过程之外周期性的让你提供单个 items 的layout 对象。比如为某 item 配置插入和删除对话。通过以下方法提供信息:

layoutAttributesForItemAtIndexPath: 所有自定义的 layout 必须重写的方法。
当返回属性时,不应该更新这些 layout 属性,如果需要改变 layout 信息,调用 invalidateLayout 在接下来的 layout 周期中更新这些信息。

两种方式设置 collection view 的 layout 为自定义的 layout,

让你的 Layout 更优异

除了上述必须实现的方法,还有一些特性能够改善自定义的 layout 的用户体验,实现这些属性是可选但是推荐实现的。

通过附加 view 提供内容品质

supplementary views 与 cells 分离并且有自己的 layout 属性,由 data source 提供,其目的是为 app 主要内容增强信息。UICollectionViewFlowLayout 中使用 supplementary view 来作为 section headers 和 footers,除此之外,可以使用 supplementary views 给每个 cell 提供一个自己的 label,用于显示 cell 的信息。与 cells 一样,supplementary views 也需要重用,所有 supplementary views 需要继承自 UICollectionReusableView 类。

添加 supplementary views 到 layout 的过程如下:

例子 自定义 collection view 布局

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray *layoutAttributes = [NSMutableArray array];
    // Cells
    // We call a custom helper method -indexPathsOfItemsInRect: here
    // which computes the index paths of the cells that should be included
    // in rect.
    NSArray *visibleIndexPaths = [self indexPathsOfItemsInRect:rect];
    for (NSIndexPath *indexPath in visibleIndexPaths) {
        UICollectionViewLayoutAttributes *attributes =
        [self layoutAttributesForItemAtIndexPath:indexPath];
        [layoutAttributes addObject:attributes];
    }

    // Supplementary views
    NSArray *dayHeaderViewIndexPaths = [self indexPathsOfDayHeaderViewsInRect:rect];
    for (NSIndexPath *indexPath in dayHeaderViewIndexPaths) {
        UICollectionViewLayoutAttributes *attributes =
        [self layoutAttributesForSupplementaryViewOfKind:@"DayHeaderView"
                                             atIndexPath:indexPath];
        [layoutAttributes addObject:attributes];
    }

    NSArray *hourHeaderViewIndexPaths = [self indexPathsOfHourHeaderViewsInRect:rect];
    for (NSIndexPath *indexPath in hourHeaderViewIndexPaths) {
        UICollectionViewLayoutAttributes *attributes =
        [self layoutAttributesForSupplementaryViewOfKind:@"HourHeaderView"
                                             atIndexPath:indexPath];
        [layoutAttributes addObject:attributes];
    }
    return layoutAttributes;
}

处理 supplementary views 布局属性的过程和 cell 属性过程是一样的,但是不同的是 supplementary views 可以有很多种但只有一种 cell。这是因为 supplementary view 与它们是分离开的,是为了烘托主旨,所以每个 supplementary view 方法都会指明其类别 kind 以方便正确计算其特有的属性。

在 layout 中添加 Decoration Views

Decoration views 是 Layout UI 特征的有效点缀,它仅仅提供一些可视化的内容,与 data source 无关。可以用来自定义背景,在 cells 缝隙之间填充,甚至可以掩盖 cell,完全由 layout 对象控制。

在 layout 中添加 Decoration view 的步骤:

decoration view 与 cell 和 supplementary view 的创建过程不同,仅需要注册 class 或者 nib 即可,最多调用一个 initWithFrame: 方法,不需要额外的配置,注意可以使用 zIndex属性来设置层级结构、任何 decoration view 需要是 UICollectionReusableView 子类,启动了回收机制。

插入和删除动画

插入新的 cell 的时候,collection view 会询问 layout 对象提供一组初始化属性用于动画,结束属性就是默认的位置、属性等。类似的,当删除一个 cell 的时候,collection view 会询问 layout 对象提供一组终值属性用于动画,初始属性默认的 indexPath 位置等。

custom_insert_animations

当插入 item 的时候,layout 对象需要提供正在要被插入的 item 的初始化 layout 信息。在此例中, layout 先将 cell 的初始位置位置到 collection view 的中间,并将 alpha 设为0,动画期间,此 cell 会渐渐出现并移动到右下角。参考代码:

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
   UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
   attributes.alpha = 0.0;
 
   CGSize size = [self collectionView].frame.size;
   attributes.center = CGPointMake(size.width / 2.0, size.height / 2.0);
   return attributes;
}

需要注意的是,上述代码在插入 cell 的时候所有的 cell 都会添加此插入的动画,若只想对插入的 item 做插入动画,可以检查 indexPath 是否与传入的prepareForCollectionViewUpdates: 方法的 item 的 indexPath 匹配,并且只有在匹配的时候才进行动画,否则只返回 super 的initialLayoutAttributesForAppearingItemAtIndexPath:. 方法。

override func prepareForCollectionViewUpdates(updateItems: [UICollectionViewUpdateItem]) {
        super.prepareForCollectionViewUpdates(updateItems)
        insertIndexPath = [NSIndexPath]()
        deleteIndexPath = [NSIndexPath]()
        for update in updateItems {            
            switch update.updateAction {
            case .Insert:
                insertIndexPath.append(update.indexPathAfterUpdate!)
            case .Delete:
                deleteIndexPath.append(update.indexPathBeforeUpdate!)
            default:
                print("error")
            }            
            if update.updateAction == UICollectionUpdateAction.Insert {
                
            }
        }
        
    }

delete 动画与插入类似,需要提供正确的 final layout 属性。

提升 layout 的滚动体验

当滚动相关的 touch 事件结束后,scrollview 会根据当前的 speed 和减速状况决定最终会停在哪个偏移。一旦 collection view 知道这个位置后,它就会询问 layout 对象是否修改这个位置,通过调用 targetContentOffset(forProposedContentOffset:withScrollingVelocity:) 。由于是在滚动过程中调用此方法,所以自定义 layout 可以改变滚动的停止位置。

下图展示了调整滚动特性的效果。

custom_target_scroll_offset

假如 collection view 开始于(0,0),且用户向左滑动,collection view 计算出滚动原本会停在如下的位置,这个值是 “proposed” content 的 offset 值。自定义 layout 可以改变这个值,以确保滚动停下的时候,某个 item 正好停留在可见区域的正中间。这个新值会成为新的目标的 content offset,这个值从 targetContentOffsetForProposedContentOffset:withScrollingVelocity: 方法中返回。

例子:

override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {        
        //计算最终显示的矩形框
        var rect: CGRect = CGRectZero
        rect.origin.y = 0
        rect.origin.x = proposedContentOffset.x
        rect.size = (collectionView!.frame.size)        
        //根据最终的矩形来获得super已经计算好的属性
        let originArray = super.layoutAttributesForElementsInRect(rect)
        let attributes = NSArray(array: originArray!, copyItems: true) as? [UICollectionViewLayoutAttributes]
        //计算collectionView最中心点的x值
        let centerX = proposedContentOffset.x + collectionView!.frame.size.width * 0.5
        //存放做小间距
        var minDelta: CGFloat = CGFloat(MAXFLOAT)
        for attrs in attributes! {
            if abs(minDelta) > abs(attrs.center.x - centerX) {
                minDelta = attrs.center.x - centerX
            }
        }
        //修改原有的偏移量
        return CGPointMake(proposedContentOffset.x + minDelta, proposedContentOffset.y)
        
    }

改进自定义布局的建议

Demo

CollectionView Demo

Github地址:CollectionView Demo

学习方法

如何查询 Apple 官方文档?

参考资料

上一篇下一篇

猜你喜欢

热点阅读