UICollectionView自定义布局教程

转载自:叶孤城

UICollectionView自定义布局Pintere

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循环里的逻辑判断.

override func prepareLayout() {  // 1
  if cache.isEmpty {    // 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](count: numberOfColumns, repeatedValue: 0)    // 3
    for item in 0 ..< collectionView!.numberOfItemsInSection(0) {      let indexPath = NSIndexPath(forItem: item, inSection: 0)      // 4
      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
      let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
      attributes.frame = insetFrame
      cache.append(attributes)      // 6
      contentHeight = max(contentHeight, CGRectGetMaxY(frame))
      yOffset[column] = yOffset[column] + height

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

继续承接上一篇,在prepareLayout()里的这个计算Item的frame是整个Layout的核心,所以必须要每一段话都读懂才能领会UICollectionViewLayout的核心.

  • var column = 0:这个变量是干吗的呢?我们上文贴过这么一张图.

我解释过,在这个布局中,所谓列这个概念是人工加上去的.在CollectionView中,没有所谓的竖排的这种列,只有Section和Item(也就是Row).而排列方式总是从左到右,一行排满了到下一行这种方式.

而我们的第二个Item的Y轴依赖于第0个Item的高度,所以,你也可以看做是第偶数个Item的Y轴总是和上一个偶数Item的高度相关.奇数的Item也类似.

那么,作者又是怎么计算的呢?

他采取了另外一种方式,声明了一个yOffset数组,只有两个数值.yOffset[0]记录第一列的上一个Item的height,yOffset[1]记录第二列的上一个Item的height.

当for循环走到第一列的Item(也就是index为偶数的Item)的时候他就去yOffset[0]里去取,走到第二列的Item(也就是index为奇数的Item)的时候就去yOffset[1]里去拿.

column就是作者用来判别到底当前循环走到的Item是第一列还是第二列的.

不信?看这句

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

我们来走一下,column声明时赋值为0.第一个for循环走完之后,这个判断是

column(现在是0) >= (numberOfColumns(始终为2,因为是定死的只有两列) - 1) ? 0: ++column

所以第一遍循环完了之后column变成1了,说明第二次要从yOffset[1]里取值了.看懂了么?然后每次循环的时候,column就在0和1之间变啊变.

其实看懂了这一句,其他的就没什么难度了,无非是从Delegate里拿到图片高度,文字标题高度,文字内容高度,加起来,就是每一个Item的height,然后依据这个Item所在的是第一列,给放到yOffset数组里就行了.

最后把每一个LayoutAttributes存到数组里,用来以后判断和读取用.以后就不需要再计算了.

返回布局的ContentSize

在for循环里,我们每执行一次之后都要做一次这个判断.

contentHeight = max(contentHeight, CGRectGetMaxY(frame))

其实cotnentSize的height不就是最后的那个Cell的CGRectGetMaxY(frame).所以可以这么计算.

override func collectionViewContentSize() -> CGSize {
  return CGSize(width: contentWidth, height: contentHeight)
}

重写layoutAttributesForElementsInRect(_:)

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

这个方法的含义我上一篇已经讲过了,就是每次CollectionView滚动到某个区域的时候,CollectionView需要知道这个区域里的每个Cell的layoutAttributes.

那我们就用CGRectIntersectsRect这个方法遍历判断有哪些attributes的frame在这个区域里,然后返回给collectionView就行了.

一个获取Photo的height的代理方法

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.size.height
  }

这个代理方法是计算Image在contentMode为aspectFit的UIImageView里压缩过之后,如何计算高度的.如果看不懂的可以看武蕴牛X(这个ID真的狂拽炫酷叼霸天)的这篇blog.

最后一步,修改我们的UICollectionViewCell中的UIImageView的height这个constraint

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


是重写CollectionViewCell的这个方法,在每次给我们的Cell赋layoutAttributes之后,拿到我们的Photo高度,然
后把cell的imageViewHeightLayoutConstraint拉一条线,更改Constant属性为layoutAttributes
中的photoHeight就行了.

完结的项目在这里

下一期,带领大家做一个英语流利说的这种布局效果的Layout.

时间: 2024-11-03 21:58:21

UICollectionView自定义布局教程的相关文章

自定义UICollectionViewController之后 如何设置UICollectionView的布局方式

我们很多时候使用UICollectionView 可能都是直接创建 UICollectionView   通过初始化的时候  传入一个布局对象的方式来使用UICollectionView 比如我们之前是这样写得: 1 UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; 2 3 UICollectionView *collection = [[UICollectionView alloc]

自定义UICollectionViewController之后 如何设置UICollectionView的布局方式--备用

我们很多时候使用UICollectionView 可能都是直接创建 UICollectionView   通过初始化的时候  传入一个布局对象的方式来使用UICollectionView 比如我们之前是这样写得: 1 UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; 2 3 UICollectionView *collection = [[UICollectionView alloc]

Swift - 使用网格(UICollectionView)的自定义布局实现复杂页面

网格UICollectionView除了使用流布局,还可以使用自定义布局.实现自定义布局需要继承UICollectionViewLayout,同时还要重载下面的三个方法: 1 2 3 4 5 6 7 8 9 10 11 12 // 这个方法返回每个单元格的位置和大小 override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath)     -> UICollectionViewLayoutAttributes! {

iOS开发之窥探UICollectionViewController(三) --使用UICollectionView自定义瀑布流

上篇博客的实例是自带的UICollectionViewDelegateFlowLayout布局基础上来做的Demo, 详情请看<iOS开发之窥探UICollectionViewController(二) --详解CollectionView各种回调>.UICollectionView之所以强大,是因为其具有自定义功能,这一自定义就不得了啦,自由度非常大,定制的高,所以功能也是灰常强大的.本篇博客就不使用自带的流式布局了,我们要自定义一个瀑布流.自定义的瀑布流可以配置其参数: 每个Cell的边距

Android自定义视图教程

Android自定义视图教程 Android的UI元素都是基于View(屏幕中单个元素)和ViewGroup(元素的集合),Android有许多自带的组件和布局,比如Button.TextView.RelativeLayout.在app开发过程中我们需要自定义视图组件来满足我们的需求.通过继承自View或者View的子类,覆写onDraw或者onTouchEvent等方法来覆盖视图的行为. 创建完全自定义的组件 创建自定义的组件主要围绕着以下五个方面: 绘图(Drawing): 控制视图的渲染,

&lt;转载&gt;Div+Css布局教程(-)CSS必备知识

目录: 1.Div+Css布局教程(-)CSS必备知识 注:本教程要求对html和css有基础了解. 一.CSS布局属性 Width:设置对象的宽度(width:45px). Height:设置对象的高度(Height:45px;). Background:设置对象的背景颜色.背景图像. 1.背景颜色 background:#09F; 2.背景图像 background:url(file:///C|/Users/Administrator/Desktop/huipu.jpg) repeat-x;

ActionBar 自定义布局定义

Android系统中ActionBar默认的布局不美观且难于控制,通过为ActionBar自定义布局的方式可以灵活控制ActionBar. 效果: 工具/原料 android集成开发环境eclipse.ADT android sdk 3.0及以上 方法/步骤 自定义Activity主题和ActionBar样式 在新建的android工程的res/values/styles.xml添加自定义ActionBar样式的代码和自定义Activity主题的代 码,并在AndroidMainfest.xml

Notification的基本用法以及使用RemoteView实现自定义布局

Notification的作用 Notification是一种全局效果的通知,在系统的通知栏中显示.既然作为通知,其基本作用有: 显示接收到短消息.即时信息等 显示客户端的推送(广告.优惠.新闻等) 显示正在进行的事物(后台运行的程序,如音乐播放进度.下载进度) Notification的基本操作: Notification的基本操作主要有创建.更新和取消三种.一个Notification的必要属性有三项,如果不设置的话在运行时会抛出异常: 小图标,通过setSmallIcon方法设置 标题,通

Android开发学习之路--UI之自定义布局和控件

新的一年已经开始了,今天已经是初二了,两天没有学习了,还是要来继续学习下.一般手机的title都是actionbar,就像iphone一样可以后退,可以编辑.这里自定义布局就来实现下这个功能,首先准备下三张图片,一张用来当作背景,两张分别表示后退和编辑.新建工程UICostomViewsTest,然后自动创建工程后,新建title.xml,编写代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearL