UICollectionView 是在 iOS6 时出现的,不少人都会把它和 UITableView 作一个比较,相比而言UICollectionView 要更强大、同时要比 UITableView 的布局结构更加灵活彻底。像瀑布流这样的灵活布局就是用 UICollectionView 来实现的。这篇文章咱们主要就来了解一下 UICollectionView 是怎样实现这样的灵活布局的。swift
UICollectionView 的灵活主要得益于UICollectionViewLayout,那么 UICollectionViewLayout 是什么呢?bash
UICollectionViewLayout 是什么呢?咱们首先 来看一下 官方 API 给出的解释:app
The UICollectionViewLayout class is an abstract base class that you subclass and use to generate layout information for a collection view. The job of a layout object is to determine the placement of cells, supplementary views, and decoration views inside the collection view’s bounds and to report that information to the collection view when asked. The collection view then applies the provided layout information to the corresponding views so that they can be presented onscreen.ide
大体的意思是:UICollectionViewLayout类是一个抽象类,并使用生成UICollectionView的布局信息。他布局时候的工做是来肯定 Cell 的位置的设置等一些布局的信息。而后UICollectionView提供的布局信息适用于相应的视图,这样他们就能够呈如今屏幕上。布局
总而言之一句话就是: UICollectionViewLayout 决定了 CollectionView 的布局样式(cell 的排列方式)ui
既然咱们已经知道了 UICollectionView 的灵活布局全靠UICollectionViewLayout,那么应该怎么使用UICollectionViewLayout就能够作出瀑布流这样的布局呢?spa
首先必须写一个UICollectionViewLayout的子类,并重写UICollectionViewLayout的方法,咱们以写瀑布流为例:看一下示例代码code
//
// YHYCollectionViewLayout.swift
// mapps
//
// Created by 太阳在线YHY on 2017/3/1.
// Copyright © 2017年 太阳在线. All rights reserved.
//
@objc protocol YHYCollectionViewLayoutDelegate {
//waterFall的列数
func columnOfWaterFall(_ collectionView: UICollectionView) -> Int
//每一个item的高度
func waterFall(_ collectionView: UICollectionView, layout waterFallLayout: YHYCollectionViewLayout, heightForItemAt indexPath: IndexPath) -> CGFloat
}
import UIKit
class YHYCollectionViewLayout: UICollectionViewLayout {
var delegate: YHYCollectionViewLayoutDelegate?
// 列数 默认是2
@IBInspectable var columnCount: CGFloat = 2
// 列间距 默认是0
@IBInspectable var columnSpacing: CGFloat = 0
// 行间距 默认是0
@IBInspectable var lineSpacing: CGFloat = 0
// section 和 collectionView 的间距 默认是(0,0,0,0)
@IBInspectable var sectionInsets: UIEdgeInsets = UIEdgeInsets.zero
// setctionTop
@IBInspectable var sectionTop: CGFloat = 0 {
willSet {
sectionInsets.top = newValue
}
}
@IBInspectable var sectionBottom: CGFloat = 0 {
willSet {
sectionInsets.bottom = newValue
}
}
@IBInspectable var sectionLeft: CGFloat = 0 {
willSet {
sectionInsets.left = newValue
}
}
@IBInspectable var sectionRight: CGFloat = 0 {
willSet {
sectionInsets.right = newValue
}
}
// 每行对应的高度
private var columnHeights: [Int: CGFloat] = [Int: CGFloat]()
private var attributes: [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
// 自定义初始化方法 (由于这里要定义瀑布流的效果,因此初始化方法设定了 行间距 列间距 和 section之间的设定)
init(lineSpacing: CGFloat,columnSpacing: CGFloat, sectionInsets: UIEdgeInsets) {
super.init()
self.lineSpacing = lineSpacing
self.columnSpacing = columnSpacing
self.sectionInsets = sectionInsets
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// 重写父类方法 子类必须重写此方法,并使用它返回collectionView的内容的宽度和高度。 这些值表示全部内容的宽度和高度,而不单单是当前可见的内容。 collectionView使用此信息来配置其本身的内容大小以用于滚动目的。
override var collectionViewContentSize: CGSize {
var maxHeight: CGFloat = 0
for height in columnHeights.values {
if height > maxHeight {
maxHeight = height
}
}
return CGSize(width: collectionView?.frame.width ?? 0, height: maxHeight + sectionInsets.bottom)
}
// 重写 prepare方法 ,这个方法必须写,它是用来告诉 layout 要更改当前的布局,也能够在这个方法里作一些准备的工做
override func prepare() {
super.prepare()
guard collectionView != nil else {
return
}
if let columnCount = delegate?.columnOfWaterFall(collectionView!) {
for i in 0..<columnCount {
columnHeights[i] = sectionInsets.top
}
}
let itemCount = collectionView!.numberOfItems(inSection: 0)
attributes.removeAll()
for i in 0..<itemCount {
if let att = layoutAttributesForItem(at: IndexPath.init(row: i, section: 0)) {
attributes.append(att)
}
}
}
// 重写 layoutAttributesForItem 方法 用来计算每一个 cell 的大小 子类必须重写此方法,并使用它来返回集合视图中项目的布局信息。 您可使用此方法仅为具备相应单元格的项目提供布局信息。
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if let collectionView = collectionView {
// 根据 indexPath 获取 item 的 attributes
let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
// 获取 collectionView 的宽
let width = collectionView.frame.width
//
if let columnCount = delegate?.columnOfWaterFall(collectionView) {
// columnCount 是 collectionView 中 item 的列数
guard columnCount > 0 else {
return nil
}
// item 的宽度 = (colelctionView 的 宽 - 边距 - 列间距)/ 列数
// 每一行总的 item 的宽度
let totalWidth = (width - sectionInsets.left - sectionInsets.right - (CGFloat(columnCount) - 1) * columnSpacing)
// 每一个 item 的宽度
let itemWidth = totalWidth / CGFloat(columnCount)
print(itemWidth)
// 计算 item 的高度
let itemheight = delegate?.waterFall(collectionView, layout: self, heightForItemAt: indexPath) ?? 0
// 找出最短的一列
var minIndex = 0
for column in columnHeights {
if column.value < columnHeights[minIndex] ?? 0 {
minIndex = column.key
}
}
// 根据 最短列的列数计算 item 的 x 值
let itemX = sectionInsets.left + (columnSpacing + itemWidth) * CGFloat(minIndex)
// item 的 y 值 = 最短列的最大+ 行间距
let itemY = (columnHeights[minIndex] ?? 0) + lineSpacing
// 设置 attributes 的 frame
attribute.frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: itemheight)
// 更新字典中的最大 y 值
columnHeights[minIndex] = attribute.frame.maxY
}
return attribute
}
return nil
}
//子类必须重写此方法,并使用它返回视图与指定矩形相交的全部项的布局信息。 您的实现应该返回全部可视元素的属性,包括单元格,补充视图和装饰视图。
//建立布局属性时( layout attributes),始终建立表示正确元素类型(单元格,补充或装饰)的属性对象。 集合视图区分每种类型的属性,并使用该信息来决定要建立的视图以及如何管理它们。
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributes
}
}
复制代码
如何自定义代码中有很详细的解释,都是经过重写父类的方法来从新构建 collectionView 的布局样式的,具体的布局方式要依靠本身在重写父类方法的时候具体来计算才行。因此思想很简单,关键在于布局的计算方法,想要作出不一样的样式就要依靠强大的计算方式来实现。orm
下面再贴一段环形布局方式的代码:能够研究一下他又是如何来计算出布局的:cdn
//
// YHYCircleCollectionViewLayout.swift
// mapps
//
// Created by 太阳在线YHY on 2017/3/1.
// Copyright © 2017年 太阳在线. All rights reserved.
//
import UIKit
class YHYCircleCollectionViewLayout: UICollectionViewLayout {
private var attributes: [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
@IBInspectable var center: CGPoint!
var itemCount: Int!
var radius: CGFloat!
override func prepare() {
super.prepare()
// 总共的 cell
itemCount = collectionView!.numberOfItems(inSection: 0)
center = CGPoint(x: collectionView!.frame.width / 2, y: collectionView!.frame.height / 2)
radius = min(collectionView!.frame.width, collectionView!.frame.height) / 4
attributes.removeAll()
for i in 0..<itemCount {
if let att = layoutAttributesForItem(at: IndexPath.init(row: i, section: 0)) {
attributes.append(att)
}
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: collectionView?.frame.width ?? 0, height: (collectionView?.frame.width)!)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attribute.size = CGSize(width: 60.0, height: 60.0)
// 当前cell的角度
// 注意类型转换
let angle = 2 * CGFloat(M_PI) * CGFloat(indexPath.row) / CGFloat(itemCount)
// 一点点数学转换
attribute.center = CGPoint(x: center.x + radius*cos(angle), y: center.y + radius * sin(angle))
return attribute
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributes
}
}
复制代码