UICollectionView 自定义布局 教程: Pinterest

https://www.raywenderlich.com/164608/uicollectionview-custom-layout-tutorial-pinterest-2

入门

Download the starter project 下载教程用Xcode打开。

编译并运行工程,将会看见下面这样:

image.png

这个APP显示的是来自 RWDevCon. 的照片的图片集。可以浏览照片并且可以看见这些参会者在会议上是多么的有趣。

这个图集是用UICollectionView和一个标准布局建立的。初看,感觉不错。但是这个布局的设计肯定可以优化。这些图片没有完全的展示出来,并且常文本在结尾都被截断了。

总体的用户体验表现非常无趣,因为所有的cell都是一样的size。有一个改善设计的方式是创建自定义布局让每个cell可以自由的拥有合适他的size。

创建UICollectionViewLayout自定义布局

第一步,创建一个令人震惊的colectionView需要为图集创建一个自定义布局。

collectionView布局是抽象类UICollectionViewLayout的子类。它定义了collectionView中所有可见的item的attributes。这个attributes是一个UICollectionViewLayoutAttributes类型的实例,它包含了collectionView中每个item的属性,例如frame或者transform.

在Layouts组下创建一个新的布局文件,在 iOS\Source 中选择Cocoa Touch Class。创建UICollectionViewLayout 的子类起名PinterestLayout。确保选择的语言是Swift然后创建它。

下一步需要为collectionView创建一个新的layout.

打开Main.storyboard 选择Photo Stream View Controller Scene 中的Collection View ,如下:

image.png

OK,是时候去看效果了,编译运行你的app:

image.png
image.png

不要惊慌,这是对的,不管你相不相信。这意味着collectionView使用的是你自定义的layout。这些cell没有显示出来,因为PinterestLayout没有实现布局过程中会调用的一些方法。

核心布局过程:

花一点时间考虑下collectionView的布局过程,他是需要collectionView 和 layout对象一起协作完成。当collectionView 需要布局信息,它要求layout对象提供需要特定的顺序调用某些方法:

image.png

你的layout子类不许实现下列方法:

collectionViewContentSize: 这个方法会返回collectionView的contentSize.你必须重写他。然后返回collectionView的contentSize. collectionView使用这个信息在内部去配置他的scrollView的contentSize.

prepare():每当布局操作即将发生时,就调用此方法。这是你准备并执行一些计算,计算collectionView中cell的位置和size。

layoutAttributesForElements(in:): 在这个方法中你需要为给定的矩形内的所有item返回布局属性(layout attributes)。给collectionView返回的attributes是UICollectionViewLayoutAttributes类型原素的数组。

layoutAttributesForItem(at:): 这个方法需要为collectionView提供布局需要的信息。你需要重写他,并且为IndexPath对应的item返回布局属性。

OK,现在你知道了那些方法需要去实现—但是要怎样计算这些属性呢?

计算布局属性

这种布局,你需要为每个item动态的计算他的高度,由于你不能预先知道图片的高度。你需要什么一个协议,当PinterestLayout需的时候用来提供高度信息。

现在,回到代码。打开PinterestLayout.swift 并且添加如下代理协议申明。

protocol PinterestLayoutDelegate: class {

  func collectionView(_ collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat

}

这个代码是声明了一个PinterestLayoutDelegate协议,他有一个方法去获取图片的高度。你需要在PhotoStreamViewController里面遵守这个协议并实现方法。

还有一些事情要做在实现布局方法之前,你需要声明一些属性来帮助布局过程。

在PinterestLayout添加如下代码:

// 1

weak var delegate: PinterestLayoutDelegate!

// 2

fileprivate var numberOfColumns = 2

fileprivate var cellPadding: CGFloat = 6

// 3

fileprivate var cache = [UICollectionViewLayoutAttributes]()

// 4

fileprivate var contentHeight: CGFloat = 0

fileprivate var contentWidth: CGFloat {

  guard let collectionView = collectionView else {

    return 0

  }

  let insets = collectionView.contentInset

  return collectionView.bounds.width - (insets.left + insets.right)

}

// 5

override var collectionViewContentSize: CGSize {

  return CGSize(width: contentWidth, height: contentHeight)

}

这段代码定义了一些属性,在后面的布局需要提供的。在这,一步一步解释:

1.这将保持对delegate的引用。

2.这有2个属性用来配置布局,列数和cell间距。

3.这是一个数组,用来缓存attributes。当调用prepare(),你需要为所有的item计算attributes,并添加到缓存中。当后续collectionView获取布局属性时,你可以更加高效的查下缓存,而不是每次都去计算、

4.声明2个属性去存储contentSize,contentHeight 递增添加照片,contentWidth的计算是基于集合视图的宽度和其内容的inset。

5.重写collectionViewContentSize去返回collectionView的contentSize. 使用上一步计算得到的contentHeight 和 contentWidth。

您已经准备好计算coillectionView视图items的属性,现在将包含frame。要了解这将如何完成,请看下面的图表:

image.png

你需要为每个item计算frame,基于所在的column(xOffset)和前一个item的位置(同column,yOffset)。

计算水平方向的位置,您将使用该列所属的列的开始x坐标,然后添加cell间距。垂直方向的位置,开始前一个item的位置然后加前一个item的高度。这所有的item的高度是图片的高度加内容的间距。

你需要在 prepare(),里面做这些事情。你的主要目的是计算每个item的UICollectionViewLayoutAttributes布局实例。

添加下面的方法到PinterestLayout:

override func prepare() {

  // 1

  guard cache.isEmpty == true, let collectionView = collectionView else {

    return

  }

  // 2

  let columnWidth = contentWidth / CGFloat(numberOfColumns)

  var xOffset = [CGFloat]()

  for column in 0 ..< numberOfColumns {

    xOffset.append(CGFloat(column) * columnWidth)

  }

  var column = 0

  var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)

  // 3

  for item in 0 ..< collectionView.numberOfItems(inSection: 0) {

    let indexPath = IndexPath(item: item, section: 0)

    // 4

    let photoHeight = delegate.collectionView(collectionView, heightForPhotoAtIndexPath: indexPath)

    let height = cellPadding * 2 + photoHeight

    let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)

    let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)

    // 5

    let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)

    attributes.frame = insetFrame

    cache.append(attributes)

    // 6

    contentHeight = max(contentHeight, frame.maxY)

    yOffset[column] = yOffset[column] + height

    column = column < (numberOfColumns - 1) ? (column + 1) : 0

  }

}

依次解释每个注释:

1.仅仅在缓存为空,并且collectionView存在的时候去计算布局属性。

2.声明并填充每一列的列宽的基于坐标xoffset阵列。使yOffset阵列跟踪每柱坐标。你yoffset初始化每个值0,因为这是第一项在每个柱偏移。

3.循环第一组的所以item,这个特别的布局只有第一组。

4.这是执行frame的计算,width是之前计算的cellWidth,移除cell之间的间距。调用代理获取图片高度,frame的高度是图片的高度加上之前定义的顶部和底部的cellPadding。然后在当前的column合并x,y的offset,创建insetFrame,在attributes中用到。

5.创建UICollectionViewLayoutAttribute实例,用insertFrame设置他的frame,并将attributes添加到缓存.

6.取contentHeight 与 新计算的item的frame的maxY的更大值。然后设置当前column的yOffset。最后算出下一列是什么。

现在你需要重写 layoutAttributesForElements(in:),他是在 prepare() 调用之后调用,去决定哪些item在可见的矩形框中。

添加下面的代码到PinterestLayout文件末尾:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

  var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()

  // Loop through the cache and look for items in the rect

  for attributes in cache {

    if attributes.frame.intersects(rect) {

      visibleLayoutAttributes.append(attributes)

    }

  }

  return visibleLayoutAttributes

}

在这便利缓存中的attributes,检测是否在可见矩形框内,提供给collectionView使用。

最后必须实现的方法layoutAttributesForItem(at:):

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {

  return cache[indexPath.item]

}

在这里你只检索并从缓存获取indexpath对应的属性。

在这之前你可以看看布局操作,需要实现布局代理方法。PinterestLayout会返回图片和文本的高度用来计算attributes的frame。

打开 PhotoStreamViewController.swift 并添加如下代码:

extension PhotoStreamViewController: PinterestLayoutDelegate {

  func collectionView(_ collectionView: UICollectionView,

                      heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {

    return photos[indexPath.item].image.size.height

  }

}

在这获取图片准确的高度。

下一步,添加下面的代码到 viewDidLoad() 中,紧挨着super:

if let layout = collectionView?.collectionViewLayout as? PinterestLayout {

  layout.delegate = self

}

这是设置PhotoStreamViewController为布局代理。

是时候去看看效果了:

image.png

最终代码下载

你可能感兴趣的:(UICollectionView 自定义布局 教程: Pinterest)