在 dribbble.com 搜索 timeline 可以搜到不少优秀的原型设计。在 Github 上找了下好像没有现成的布局,有一个实现了类似 Path 的效果但不是使用布局实现,有一个是用于 Mac 平台的,于是动手实现了下,Demo 地址:TimelineLayout。本以为这类布局可以通用的,动手实现了两个例子发现很难实现一个非常通用的布局,需要根据具体的场景进行选择。
初次接触这类布局的人最大的疑惑估计是怎么生成那条轴线,其实线只不过是宽度比较小的矩形而已,这么说你就明白了吧。那么这类布局里这条轴线可以用多种方法实现,SupplementaryView (也就是通常说的 HeaderView 和 FooterView)和 DecorationView 都可用来实现这条线,甚至使用 Cell 来实现这条线也是可以的,具体要看你的场景要求来进行选择。
在 dribbble.com 上很多针对 iPhone 设计的时间轴布局用 UITableView 来实现更方便一点,比如下面几种,这几种的共同特点是,元素的种类相同,位置相对固定。实现时在固定位置插入窄矩形视图当作线条,当节点的圆形用图片搞定或者代码画出来,基本用不上布局,用 UICollectionView 实现就是多此一举了。
下面这两种就需要 UICollectionView 了,左边的链接在这里,右边的链接在这里。这两个布局其实是很普通的 FlowLayout,左边的是垂直滚动的 FlowLayout 加上了一条轴线,右边的是横向滚动的 FlowLayout 加一条轴线。DecorationView 非常适合用来实现这种轴线。Demo 地址:TimelineLayout。
DecorationView 的使用场景较少,特别是扁平化设计普及后更加少见了,也很少看到有关使用 DecorationView 的教程,不过 DecorationView 在时间轴这类布局里非常有用。Mark Pospesel 的这篇三年前的文章 How to Add a Decoration View to a UICollectionView 依然值得一看,而这篇文章的主体 IntroducingCollectionViews 实现了多种布局并包含了一份详细介绍 UICollectionView 各部分的 keynote,值得一颗星。
只使用 FlowLayout 本身自然是无法实现上述的布局的,这意味着使用UICollectionViewFlowLayout
子类,关于自定义布局入门,推荐官方文档,详细说明了布局流程,什么时候自定义布局以及需要重写哪些方法;还有 Objc.io 出品的自定义 Collection View 布局也是好文章。
自定义布局的主要流程是:
1.prepareLayout
:做一些准备工作。
2.collectionViewContentSize
:返回 collectionView 的内容尺寸用于滚动。
3.layoutAttributesForElementsInRect:
:最关键的部分,返回指定区域(也就是可视区域)内所有的布局属性,根据这些属性来配置所有 Cell, SupplementaryView 和 DecorationView 的布局。
DecorationView 并不是数据驱动的视图,它的数量以及布局完全由 CollectionView 的布局属性决定。上面的两种布局主要在 FlowLayout 的基础上添加了 DecorationView 布局属性,这三个方法只用重写第3个方法,外带实现 DecorationView 的默认布局。
添加 DecorationView
DecorationView 必须是UICollectionReusableView
的子类,添加前必须在UICollectionViewLayout
里注册,有两种注册方法:
1 2 |
func registerClass(_ viewClass: AnyClass?, forDecorationViewOfKind elementKind: String) func registerNib(_ nib: UINib?, forDecorationViewOfKind elementKind: String) |
方法里的elementKind
参数和 Cell 中的 ReuseIdentifier 作用相同。在两个例子里我自定义了UICollectionReusableView
的子类LineView
类,唯一的作用是将其背景色设置为白色。在自定义的UICollectionViewLayout
类初始化方法里注册LineView
:
1 |
self.registerClass(LineView.self, forDecorationViewOfKind: "LineView") |
同时,记得在自定义布局类中提供 DecorationView 布局对象的默认实现,即使只是提供一个空的布局对象。文档告诉我们应该这么做,我没在意,直接生成了空的布局对象,绝大部分情况下都没有问题,但后来掉进了某个坑里,所以切记重写这个方法,下面的例子里都提供下面的空布局实现:
1 2 3 |
override func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { return UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind, withIndexPath: indexPath) } |
在自定义 FlowLayout 的layoutAttributesForElementsInRect:
里添加需要的 DecorationView 布局属性即可:
1 2 3 4 5 6 7 8 9 10 11 |
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let layoutAttrs = super.layoutAttributesForElementsInRect(rect) ...... let decorationViewLayoutAttr = self.layoutAttributesForDecorationViewOfKind("LineView", atIndexPath: headerLayoutAttr.indexPath) if decorationViewLayoutAttr != nil{ layoutAttrs?.append(decorationViewLayoutAttr!) } /*修改 decorationViewLayoutAttr 的属性满足你的需求*/ ...... return layoutAttrs } |
这样就添加了一个 DecorationView 到 CollectionView 里。
时间轴布局1
Demo 地址:TimelineLayout,实际效果以及结构分解:
看图说话,这样一来也没什么好解释了的吧。在这个布局里,使用 DecorationView 来作为轴线是最优解。至于前面的 section 只有 Header 没有 Footer,这个好办,让 CollectionView 的 delegate 对象遵守UICollectionViewDelegateFlowLayout
协议并提供相关的尺寸信息就好了。
1 2 3 4 5 6 7 8 9 10 |
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { if section == sectionCount - 1{ //实际上这里的提供的 width 并不能决定 FooterView 的宽度,只不是0或负数都可以。实际的值在 Layout 里决定。 //默认情况下 FooterView 的 width 与 CollectionView 的 contentSize 的 width 值一致。 return CGSize(width: 50, height: 2) }else{ //尺寸为 Zero 时没有 FooterView,实际上返回的 CGSize 中的 width 或 height 只要有一个为0或负数也会有同样的效果。 return CGSizeZero } } |
HeaderView 和 FooterView 的尺寸效应是一样的。
在布局里,需要 Cell, SupplementaryView, DecorationView 这三种视图在布局上精准配合防止露馅,同时修改 sectionInset 为 DecorationView 留出视觉上的空间。最核心的layoutAttributesForElementsInRect:
方法如下: