UICollectionView自定义布局教程:Pinterest (1)(翻译自raywenderlich)

来源:互联网 发布:梦幻西游淘宝卖祥瑞 编辑:程序博客网 时间:2024/06/05 02:37

UICollectionView自定义布局教程:Pinterest

开始之前先说个题外话,前段时间在流利说,老板在清华的大学同学刚好是Pinterest里的第二个华人员工和第一个安卓工程师(好像是前十五号员工),被老板请来做了一次分享,经历颇为传奇. 

他之前在美国谷歌本部的Android部门(当时的薪资已经碾压大部分程序猿了),后来看到Pinterest这个app在iOS上非常火,但是还没有安卓版本,于是用了一个周六周日做了一个安卓版本,发到了Pinterest的HR那里,于是乎,人就成功跳槽了.现在已经财务自由.
(Pinterest的AB Test已经玩出花了.有机会肉身翻墙的同学一定要翻去硅谷干两年.) 

所以现在有很多朋友问,刚学iOS出来没有工作经验怎么找工作啊之类的,举个简单的例子,你想去滴滴打车啊, 下厨房啊, 英语流利说啊之类的,你直接把app down下来,unzip一下,把图片什么乱七八糟的拿出来,仿个7,8成的UI和功能,我就问,这个公司要不要你?

UICollection这个东西是在iOS6被推出来的,所以如果你的app还在支持iOS5还是老实用TableView吧,要么牛逼的就用ScrollView手撸一个出来. 

它最牛逼的地方就在于,Custom的Layout可以玩出无限可能.举个简单例子,早年有个非常出名的CoverFlow第三方库iCarousel(大约在13年的时候我非常频繁的使用过它),效果非常炫,但问题是它是用scrollview撸出来的,虽然里面也会cache 一些view来复用,保持流畅性,但是始终没有collectionView + layout来的流畅.而且如果是会玩的程序猿,真的能写出非常炫酷效果的layout. 

首先在 这个地址.把Raywenderlich的start Project下载下来. 

跑一下.效果是这样的.


效果没啥稀奇的,就是最简单的UICollectionFlowLayout效果,把一个个的Cell从左到右排,如果右边到屏幕头了,放不下了就跑到下一行继续从左到右排列一个个Cell.collectionview会根据你有没有设置minimumInteritemSpacing来设置你的每个cell的最小间距,和minimumLineSpacing来设置一行和一行的最小间距. 

其实,tableView说白了,完全可以自定义一种Layout,通过CollectionView来实现. 

我们浏览一下文件结构.


Controllers 里没啥好说的,就是一个ViewController. 

Extensions 里写了一个UIImage的分类,用来Decompression,我看了一下,其实是用UIGraphicsGetImageFromCurrentImageContext重新生成了一个UIImage. 

Models里就是一个Photo的model,包括一个图片,一个图片的标题和留言.这个model构成了我们UICollectionViewCell的内容. 

还有一个heightForComment方法,是通过boundingRectWithSize方法来计算文字内容在label里的高度.
Assets 就是我们的图片资源和文字资源. 

好了,现在我们新建一个类,继承自UICollectionViewLayout(注意,不是UICollectionViewFlowLayout).起名叫PinterestLayout,放在我们的Layout的Group里. 

然后在storyboard里选中我们的CollectionView.如图.


打开Attributes Inspector,进行如图所示的操作. 


OK,直接跑起来. 


啥都没有!



啥都没有就对了,你新建了一个Layout,里面啥都没写,肯定没有任何效果. 

Core Layout Process(核心布局的处理过程)

先看看UICollectionView和UICollectionViewLayout是怎么配合工作的. 


当你继承了一个Layout之后,有三个方法是必须Override得. 

  • prepareLayout():这个方法是干嘛的?这个方法就是当你的布局快要生效的时候,你会在这个方法里计算好每个Item的position和CollectionView的size.展开一下,最简便的提升TableView的流畅度的方法是什么?很简单,别在HeightForRow的代理方法里直接计算高度.而是在网络拉取所有数据之后计算好高度,放在Array里,直接在代理方法里return heightArray[indexPath.row].那么为什么要在prepareLayout里计算每个item的Position,意图也很明显了.就是别让系统每次滚动的时候再去计算每个Cell的frame.(如何提升tableView的performance去看VVbo的Demo.) 

  • collectionViewContentSize(): 这个方法的意思也很简单,就是返回CollectionView的ContentSize.是ContentSize而不是Size. 

  • layoutAttributesForElementsInRect(_:):在这个方法里返回某个特定区域的布局的属性.有点绕是吧,那我简单点说.eg.有一个CollectionView,ContentSize是(320, 1000), size是(320, 400),这时候我滑滑滑,滑到了(0, 544, 320, 400).好,那么在这个区域,有几个Cell,每个Cell的位置都是怎么样的?就是通过这个方法获知的.你不告诉CollectionView,他怎么知道怎么放cell,对吧. 

好的,我们现在理一下思路.
看上面那张图,A代表CollectionView,B代表Layout. 

A先问B,我cup(size)是多少,C还是D? - -!.
B告诉他.
A又问:我的ContentSize是多少.
B告诉他.
A这时候的offSet发生了变化,每滑动一下,A都会问,我现在这个位置,有几个Cell,每个Cell的位置,Transform,是怎样的?
B告诉他. 

就是这样. 

Calculating Layout Attributes (计算布局的属性)

好的,正式开始我们的编写Pinterest之旅. 

那么问题来了,现在面临的最棘手的问题是什么? 

注意这张图.


每个Cell的宽度固定,长度不定. 

这就是整个Layout的核心问题. 

所有的难度都在于,如何获知每个Item的height. 

刚才介绍Photo这个Model,我说了决定Cell高度的只有三个,1.图片高度2title的高度3.内容的高度. 

怎么获取图片的高度和文字的高度?
代理呗. 

在layout里声明一个PinterestLayoutDelegate

protocol PinterestLayoutDelegate {  // 1  func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath,       withWidth:CGFloat) -> CGFloat  // 2  func collectionView(collectionView: UICollectionView,       heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat}

第一个就是通过代理拿到图片的高度,第二个是通过代理拿到文字的高度. 

通过代理拿到了我们想要的数据,接下来,就是要在prepareLayout里计算item的Frame了. 

直接看代码. 

override func prepareLayout() {    // 1. Only calculate once    if cache.isEmpty {      // 2. Pre-Calculates the X Offset for every column and adds an array to increment the currently max Y Offset for each column      // 每列宽度      let columnWidth = contentWidth / CGFloat(numberOfColumns)      var xOffset = [CGFloat]()      // 其实就是xOffset就是两个,都是固定的.      for column in 0 ..< numberOfColumns {        xOffset.append(CGFloat(column) * columnWidth )      }      var column = 0      var yOffset = [CGFloat](count: numberOfColumns, repeatedValue: 0)      // 3. Iterates through the list of items in the first section      for item in 0 ..< collectionView!.numberOfItemsInSection(0) {        let indexPath = NSIndexPath(forItem: item, inSection: 0)        // 4. Asks the delegate for the height of the picture and the annotation and calculates the cell frame.        // 这个width是为了计算comment的长度的.        let width = columnWidth - cellPadding*2        let photoHeight = delegate.collectionView(collectionView!, heightForPhotoAtIndexPath: indexPath , withWidth:width)        let annotationHeight = delegate.collectionView(collectionView!, heightForAnnotationAtIndexPath: indexPath, withWidth: width)        let height = cellPadding +  photoHeight + annotationHeight + cellPadding        let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)        let insetFrame = CGRectInset(frame, cellPadding, cellPadding)        // 5. Creates an UICollectionViewLayoutItem with the frame and add it to the cache        let attributes = PinterestLayoutAttributes(forCellWithIndexPath: indexPath)        attributes.photoHeight = photoHeight        attributes.frame = insetFrame        cache.append(attributes)        // 6. Updates the collection view content height        contentHeight = max(contentHeight, CGRectGetMaxY(frame))        yOffset[column] = yOffset[column] + height        column = column >= (numberOfColumns - 1) ? 0 : ++column      }    }  }

我写了一点注释,方便大家观看. 

  • 第一句if cache.isEmpty:判断缓存Item高度的Array是否为空,是空则需要计算. 
  • let columnWidth = contentWidth / CGFloat(numberOfColumns):计算每列宽度,每列宽度是固定的,就是collectionView的contentWidth除以2,三列就除以3. contentWidth就是用CollectionView的Bounds.width - ContentInset里的左和右Inset. 
  • var xOffset = [CGFloat]():用来存每个Item的X坐标,其实所有Item的X坐标就只有两个. 
  • var yOffset = [CGFloat](count: numberOfColumns, repeatedValue: 0):初始化每列的Item的Y坐标,是一个数组,里面有两个元素. 

Y坐标这个东西有点绕,先看一张图. 


这个CollectionView分为两列,实际上呢?CollectionView里压根就没有列的概念.因为排列的时候始终是从左到右排列.如图. 


但是,现在呢,第二个Cell的Y轴实际上是和第零个Cell的height相关的,而第三个是和第一个相关的. 

所以yOffset这个数组里存了两个值,当第一列的Cell计算高度的时候,他会去yOffset[0]里拿数据,因为yOffset[0]只存第一列的上一个cell的height,那么同理,当走到第二列的时候,又会去yOffset[1]里拿第二列的上一个cell的height. 

整个流程他用了这么一句话判断. 

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

仔细研读for循环里的逻辑判断. 

(待续)

0 0
原创粉丝点击