Custom Collection View Layouts(一)

最近把 Ray 家的 Custom Collection View Layouts 系列视频刷了一遍,做一下记录

在开始前,先来看一下复习下基本知识:

UICollectionView 向 UICollectionViewLayout 询问布局,当询问过程发生时,layout 对象会创建 UICollectionViewLayoutAttributes 实例。一个 UICollectionViewLayoutAttributes 对象管理着一个对应的 item layout 相关信息(一对一关系)

来看一下第一部分的目标,我们要实现一个类似 Pinterest 的东西,有如下特性:

一、Basic Layout

要实现上图的目标,我们先来解决布局的问题,为此要定义一个 UICollectionViewLayout 的子类,原因如下:

自定义一个 UICollectionViewLayout 的子类 PinterestLayout,一般在子类中要重写三个方法:

  1. override func collectionViewContentSize() -> CGSize 返回所有内容的 contentSize,不仅仅是当前可见的

    override func collectionViewContentSize() -> CGSize {
        return CGSize(width: width, height: contentHeight)
    }
    
  2. override func prepareLayout() 计算每一个 item 的 size,然后为每一个 item 创建对应的 attributes,并设置 frame,最后放到缓存中去

    override func prepareLayout() {
        if cache.isEmpty {
            // 每个 item 的宽度是固定的
            let columnWidth = width / CGFloat(numberOfColumns)
            var xOffsets = [CGFloat]()
            for column in 0..<numberOfColumns {
                xOffsets.append(CGFloat(column) * columnWidth)
            }
            var yOffsets = [CGFloat](count: numberOfColumns, repeatedValue: 0)
            var column = 0
            for item in 0..<collectionView!.numberOfItemsInSection(0) {
                let indexPath = NSIndexPath(forItem: item, inSection: 0)
                let height = delegate.collectionView(collectionView!, heightForItemAtIndexPath: indexPath)
                let frame = CGRect(x: xOffsets[column], y: yOffsets[column], width: columnWidth, height: height)
                let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
                attributes.frame = frame
                cache.append(attributes)
                contentHeight = max(contentHeight, CGRectGetMaxY(frame))
                yOffsets[column] = yOffsets[column] + height
                column = column >= (numberOfColumns - 1) ? 0 : ++column
          }
        }
    }
    
  3. override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? 返回 rect 内所有 cells 和 views 的 layout attributes

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        var layoutAttributes = [UICollectionViewLayoutAttributes]()
        for attributes in cache {
            if CGRectIntersectsRect(attributes.frame, rect) {
                layoutAttributes.append(attributes)
            }
        }
        return layoutAttributes
    }
    

在上面的 prepareLayout() 中我们向 delegate 请求了 item 的高度:

let height = delegate.collectionView(collectionView!, heightForItemAtIndexPath: indexPath)  

这个 delegate 我们定义如下:

protocol PinterestLayoutDelegate {

  func collectionView(collectionView: UICollectionView, heightForItemAtIndexPath indexPath: NSIndexPath) -> CGFloat

}

我们让 UICollectionViewController 对象来实现

extension PhotoStreamViewController: PinterestLayoutDelegate {

  func collectionView(collectionView: UICollectionView, heightForItemAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    let random = arc4random_uniform(4) + 1
    return CGFloat(random * 100)
  }
}

二、Layout Attributes

我们观察 Pinterest 每个 cell 都由一个 image 和一个 annotation 组成,因此我们需要知道 image height 和 annotation height,我们可以修改 delegate 方法来返回对应的 height。除此之外,我们创建自己的 Layout Attributes,为其添加 photoHeight(因为每个 item 的 height 都是不同的),最后让 PinterestLayout 和 cell 应用我们设置的 Layout Attributes

1.首先修改 delegate 定义:

protocol PinterestLayoutDelegate {

  func collectionView(collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat
  func collectionView(collectionView: UICollectionView, heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat

}

紧接着实现 delegate,我们给每个 annotation 返回了一个固定的 height:

extension PhotoStreamViewController: PinterestLayoutDelegate {

  func collectionView(collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
    let random = arc4random_uniform(4) + 1
    return CGFloat(random * 100)
  }

  func collectionView(collectionView: UICollectionView, heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
    return 60
  }

}

最后修改 prepareLayout 中调用到 delegate 方法的地方,此时的 height = photoHeight + annotationHeight

2.创建 UICollectionViewLayoutAttributes 的子类

按照文档说明,我们创建 UICollectionViewLayoutAttributes 的子类,要覆盖以下两个方法:

  • func copyWithZone(_ zone: NSZone) -> AnyObject collection view 会拷贝 layout attribute 对象,实现此方法确保自定义的属性被拷贝

  • func isEqual(_ anObject: AnyObject?) -> Bool 在 iOS 7 之后,只有 attributes 改变之后,collection view 才会应用,如何判断改变了呢,就要用到此方法,先实现自定义属性的判断,再调用 super

具体实现如下:

class PinterestLayoutAttributes: UICollectionViewLayoutAttributes {

  var photoHeight: CGFloat = 0

  override func copyWithZone(zone: NSZone) -> AnyObject {
    let copy = super.copyWithZone(zone) as! PinterestLayoutAttributes
    copy.photoHeight = photoHeight
    return copy
  }

  override func isEqual(object: AnyObject?) -> Bool {
    if let attributes = object as? PinterestLayoutAttributes {
      if attributes.photoHeight == photoHeight {
        return super.isEqual(object)
      }
    }
    return false
  }
}

3.让 PinterestLayout 应用我们设置的 Layout Attributes

在 PinterestLayout 类中重写 layoutAttributesClass 方法

override class func layoutAttributesClass() -> AnyClass {  
    return PinterestLayoutAttributes.self
}

然后将 prepareLayout 中所有的 UICollectionViewLayoutAttributes 修改为 PinterestLayoutAttributes

4.让 cell 应用我们设置的 Layout Attributes

为了应用我们自定义的 attributes,我们需要 UICollectionReusableView 对象实现 applyLayoutAttributes: 方法

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {  
    super.applyLayoutAttributes(layoutAttributes)
    let attributes = layoutAttributes as! PinterestLayoutAttributes
    imageViewHeightLayoutConstraint.constant = attributes.photoHeight
}

三、Cell Content

最后我们来让照片等比例地缩放到 cell 的 imageView 中

为了实现这一目标,我们可以很方便地使用 AVFoundation framework 提供的方法:

func AVMakeRectWithAspectRatioInsideRect(_ aspectRatio: CGSize, _ boundingRect: CGRect) -> CGRect  

使用该方法用在 PinterestLayoutDelegate 的实现中,返回合适的 photoHeight:

extension PhotoStreamViewController: PinterestLayoutDelegate {

    func collectionView(collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
        let photo = photos[indexPath.item]
        let boundingRect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
        let rect = AVMakeRectWithAspectRatioInsideRect(photo.image.size, boundingRect)
        return rect.height
    }

    func collectionView(collectionView: UICollectionView, heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
        return 60
    }
}

最后 finished demo 地址 请自行 convert to lastest swift syntax


-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!