在Custom Collection View Layout讲到了一个mosaic布局,类似于常见的瀑布流
最终的效果如下:
Mosaic布局(类似于瀑布流)
文档摘录
官方文档内容Creating Custom Layouts
理解核心布局的过程
在布局处理的过程中,collectionview调用布局对象指定方法,其调用的顺序如下:
1.使用prepareLayout方法执行提供布局信息所需的前期计算
2.使用collectionViewContentSize方法返回内容区域的大小
3.使用layoutAttributesForElementsInRect:方法方法返回指定矩形中的单元格和视图的属性
创建布局属性
布局属性是 UICollectionViewLayoutAttributes的实例。使用如下的方法创建:
- layoutAttributesForCellWithIndexPath:
- layoutAttributesForSupplementaryViewOfKind:withIndexPath:
- layoutAttributesForDecorationViewOfKind:withIndexPath:
如果标准属性类不符合您的应用需求,则可以将其子类化并扩展,以存储有关每个视图的其他信息。当对布局属性进行子类化时,需要实现用于比较自定义属性的isEqual:
方法,因为collectionview在其某些操作中使用此方法。另外还需要遵守NSCopying
协议,实现copyWithZone:
方法。
除了定义attributes
子类之外,你的UICollectionReusableView
对象还需要实现apply(_:)
方法,以便它们可以在布局时应用任何自定义属性。
UICollectionViewLayoutAttributes类中的包含的属性:
var frame: CGRect
var bounds: CGRect
var center: CGPoint
var size: CGSize
var transform3D: CATransform3D
var transform: CGAffineTransform
var alpha: CGFloat
var zIndex: Int
var isHidden: Bool
为给定矩形中的item提供布局属性
在布局过程的最后一步,集合视图调用您的布局对象的layoutAttributesForElementsInRect:
方法。该方法的目的是为每个cell以及与指定矩形相交的每个supplementary或decoration视图提供布局属性。对于大的可滚动内容区域,集合视图可能只是要求当前可见的该内容区域的部分中的item的属性。
由于layoutAttributesForElementsInRect:
方法在prepareLayout
方法后调用,所以你应该已拥有创建或return 所需attributes的信息。layoutAttributesForElementsInRect:
方法的实现遵循如下的步骤:
- 遍历由
prepareLayout
方法生成的数据,以访问缓存的attributes或创建新的attributes。 - 检查每个item的frame,看是否与
layoutAttributesForElementsInRect:
中的矩形相交 - 对于每个相交item,将相应的
UICollectionViewLayoutAttributes
对象添加到数组。 - 将布局属性数组返回
根据你如何管理布局信息,你可以在prepareLayout
方法中创建UICollectionViewLayoutAttributes
对象,或者等到在layoutAttributesForElementsInRect:
方法中创建。但需要记住的是,缓存布局信息有许多的好处,为cell重复计算新的布局属性是一项昂贵的操作,可能会对应用程序的性能造成明显的不利影响。
根据需求提供布局的属性
collection view定期询问你的布局对象为正式布局流程之外的单个item提供属性。例如,在为项目配置插入和删除动画时,collection view会询问此信息。你的布局对象必须准备好为每个cell,supplementary view和decoration view提供布局属性。你可以通过覆盖以下方法来执行此操作:
- layoutAttributesForItemAtIndexPath:
- layoutAttributesForSupplementaryViewOfKind:atIndexPath:
- layoutAttributesForDecorationViewOfKind:atIndexPath:
每个自定义的布局类都被期望实现layoutAttributesForItemAtIndexPath:
方法
Mosaic布局实现过程
为什么要自定义布局,而不使用UICollectionViewFlowLayout
?
UICollectionViewFlowLayout
的行高按一行最大的item计算,其余的cell则居中,而且还要注意line space。所以自定义布局,继承自UICollectionViewLayout
类似于上一章中将的多列布局
在上面的效果图中,图片的高度和描述文字的高度是动态,可通过代理对象来获取
定义一个代理对象MosaicLayoutDelegate
protocol MosaicLayoutDelegate {
//image图片的高度
func collectionView(_ collectionView: UICollectionView, heightForImageAtIndexPath indexPath: IndexPath, withWidth width: CGFloat) -> CGFloat
//描述文字label的高度
func collectionView(_ collectionView: UICollectionView, heightForDescriptionAtIndexPath indexPath: IndexPath, withWidth width: CGFloat) -> CGFloat
}
另外需继承UICollectionViewLayoutAttributes
,创建一个MosaicLayoutAttributes
自定义属性,在其中添加一个变量imageHeight
,来保存图片的动态高度
class MosaicLayoutAttributes: UICollectionViewLayoutAttributes {
//图片的高度
var imageHeight: CGFloat = 0
//重写copy方法
override func copy(with zone: NSZone?) -> Any {
let copy = super.copy(with: zone) as! MosaicLayoutAttributes
copy.imageHeight = imageHeight
return copy
}
//重写isEqual方法
override func isEqual(_ object: Any?) -> Bool {
if let attributes = object as? MosaicLayoutAttributes {
if attributes.imageHeight == imageHeight {
return super.isEqual(object)
}
}
return false
}
}
有了图片高度,就要在定义的cell中改变图片高度的约束。
如下在RoundedCharacterCell
中,重写apply(_:)
方法,使用imageHeight
class RoundedCharacterCell: UICollectionViewCell {
@IBOutlet weak var characterImage: UIImageView!
@IBOutlet weak var characterTitle: UILabel!
@IBOutlet weak var characterInfo: UILabel!
//图片高度约束
@IBOutlet weak var imageViewHeightConstraint: NSLayoutConstraint!
var character: Characters? {
didSet {
if let theCharacter = character {
characterImage.image = UIImage(named: theCharacter.name)
characterTitle.text = theCharacter.title
characterInfo.text = theCharacter.description
}
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.cornerRadius = 12.0
self.layer.borderWidth = 3
self.layer.borderColor = UIColor(red: 0.5, green: 0.47, blue: 0.25, alpha: 1.0).cgColor
}
override func prepareForReuse() {
super.prepareForReuse()
characterImage.image = nil
characterTitle.text = ""
characterInfo.text = ""
}
//重写apply方法
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
let attributes = layoutAttributes as! MosaicLayoutAttributes
imageViewHeightConstraint.constant = attributes.imageHeight
}
}
在定义的布局MosaicViewLayout
中,重写如下的方法和属性:
1.重写class var layoutAttributesClass
,表示使用的attribute为MosaicLayoutAttributes
类型
2.重写prepare()
方法,做初始化计算
3.重写var collectionViewContentSize
,返回内容区域的大小
4.重写layoutAttributesForElements(in rect: CGRect)
方法
class MosaicViewLayout: UICollectionViewLayout {
//代理对象
var delegate: MosaicLayoutDelegate!
//几列
var numberOfColumns = 0
var cellPadding: CGFloat = 0
//保存计算的属性
var cache = [MosaicLayoutAttributes]()
//内容区域的高度
fileprivate var contentHeight: CGFloat = 0
//宽度
fileprivate var width: CGFloat {
get {
let insets = collectionView!.contentInset
return collectionView!.bounds.width - (insets.left + insets.right)
}
}
//内容区域的大小
override var collectionViewContentSize : CGSize {
return CGSize(width: width, height: contentHeight)
}
//表示attribute是MosaicLayoutAttributes类
override class var layoutAttributesClass : AnyClass {
return MosaicLayoutAttributes.self
}
//初始化计算
override func prepare() {
if cache.isEmpty {
let columnWidth = width / CGFloat(numberOfColumns)
var xOffsets = [CGFloat]()
for column in 0..<numberOfColumns {
xOffsets.append(CGFloat(column) * columnWidth)
}
var yOffsets = [CGFloat](repeating: 0, count: numberOfColumns)
var column = 0
for item in 0..<collectionView!.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
//cell的宽度
let width = columnWidth - (cellPadding * 2)
//图片的宽度
let imageHeight = delegate.collectionView(collectionView!, heightForImageAtIndexPath: indexPath, withWidth: width)
//描述文字的高度
let descriptionHeight = delegate.collectionView(collectionView!, heightForDescriptionAtIndexPath: indexPath, withWidth: width)
//cell的高度
let height = cellPadding + imageHeight + descriptionHeight + cellPadding
let frame = CGRect(x: xOffsets[column], y: yOffsets[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = MosaicLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
//更新imageHeight的值
attributes.imageHeight = imageHeight
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
yOffsets[column] = yOffsets[column] + height
column = column >= (numberOfColumns - 1) ? 0 : column + 1
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {//相交
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}
在控制器中设置layout的代理对象等其它属性
let layout = collectionViewLayout as! MosaicViewLayout
layout.delegate = self
layout.numberOfColumns = 2
layout.cellPadding = 5
实现MosaicLayoutDelegate
方法,返回图片和文字的高度
在计算图片的高度时使用了AVFoundation
的AVMakeRect(aspectRatio: CGSize, insideRect boundingRect: CGRect)
方法
extension MasterViewController: MosaicLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForImageAtIndexPath indexPath: IndexPath, withWidth width: CGFloat) -> CGFloat {
let character = charactersData[indexPath.item]
let image = UIImage(named: character.name)
let boundingRect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
let rect = AVMakeRect(aspectRatio: image!.size, insideRect: boundingRect)
return rect.height
}
func collectionView(_ collectionView: UICollectionView, heightForDescriptionAtIndexPath indexPath: IndexPath, withWidth width: CGFloat) -> CGFloat {
let character = charactersData[indexPath.item]
let descriptionHeight = heightForText(character.description, width: width-24)
let height = 4 + 17 + 4 + descriptionHeight + 12
return height
}
func heightForText(_ text: String, width: CGFloat) -> CGFloat {
let font = UIFont.systemFont(ofSize: 10)
let rect = NSString(string: text).boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return ceil(rect.height)
}
}