Mosaic布局(类似于瀑布流)

Custom Collection View Layout讲到了一个mosaic布局,类似于常见的瀑布流

最终的效果如下:

最终效果

Mosaic布局(类似于瀑布流)

文档摘录

官方文档内容Creating Custom Layouts

理解核心布局的过程

在布局处理的过程中,collectionview调用布局对象指定方法,其调用的顺序如下:

1.使用prepareLayout方法执行提供布局信息所需的前期计算
2.使用collectionViewContentSize方法返回内容区域的大小
3.使用layoutAttributesForElementsInRect:方法方法返回指定矩形中的单元格和视图的属性

这里写图片描述

创建布局属性

布局属性是 UICollectionViewLayoutAttributes的实例。使用如下的方法创建:

如果标准属性类不符合您的应用需求,则可以将其子类化并扩展,以存储有关每个视图的其他信息。当对布局属性进行子类化时,需要实现用于比较自定义属性的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: 方法的实现遵循如下的步骤:

  1. 遍历由prepareLayout方法生成的数据,以访问缓存的attributes或创建新的attributes。
  2. 检查每个item的frame,看是否与layoutAttributesForElementsInRect:中的矩形相交
  3. 对于每个相交item,将相应的UICollectionViewLayoutAttributes对象添加到数组。
  4. 将布局属性数组返回

根据你如何管理布局信息,你可以在prepareLayout方法中创建UICollectionViewLayoutAttributes对象,或者等到在layoutAttributesForElementsInRect:方法中创建。但需要记住的是,缓存布局信息有许多的好处,为cell重复计算新的布局属性是一项昂贵的操作,可能会对应用程序的性能造成明显的不利影响。

根据需求提供布局的属性

collection view定期询问你的布局对象为正式布局流程之外的单个item提供属性。例如,在为项目配置插入和删除动画时,collection view会询问此信息。你的布局对象必须准备好为每个cell,supplementary view和decoration view提供布局属性。你可以通过覆盖以下方法来执行此操作:

每个自定义的布局类都被期望实现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方法,返回图片和文字的高度
在计算图片的高度时使用了AVFoundationAVMakeRect(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)
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值