UICollectionView 自定义样式的实现 Swift3.0 (瀑布流等布局)

UICollectionView 是在 iOS6 时出现的,很多人都会把它和 UITableView 做一个比较,相比而言UICollectionView 要更强大、同时要比 UITableView 的布局结构更加灵活完全。像瀑布流这样的灵活布局就是用 UICollectionView 来实现的。这篇文章我们主要就来了解一下 UICollectionView 是怎样实现这样的灵活布局的。

UICollectionView 的灵活主要得益于UICollectionViewLayout,那么 UICollectionViewLayout 是什么呢?

UICollectionViewLayout 简介

UICollectionViewLayout 是什么呢?我们首先 来看一下 官方 API 给出的解释:

The UICollectionViewLayout class is an abstract base class that you subclass and use to generate layout information for a collection view. The job of a layout object is to determine the placement of cells, supplementary views, and decoration views inside the collection view’s bounds and to report that information to the collection view when asked. The collection view then applies the provided layout information to the corresponding views so that they can be presented onscreen.

大致的意思是:UICollectionViewLayout类是一个抽象类,并使用生成UICollectionView的布局信息。他布局时候的工作是来确定 Cell 的位置的设置等一些布局的信息。然后UICollectionView提供的布局信息适用于相应的视图,这样他们就可以呈现在屏幕上。

总而言之一句话就是: UICollectionViewLayout 决定了 CollectionView 的布局样式(cell 的排列方式)

UICollectionViewLayout的使用

既然我们已经知道了 UICollectionView 的灵活布局全靠UICollectionViewLayout,那么应该怎么使用UICollectionViewLayout就可以做出瀑布流这样的布局呢?

首先必须写一个UICollectionViewLayout的子类,并重写UICollectionViewLayout的方法,我们以写瀑布流为例:看一下示例代码

//
//  YHYCollectionViewLayout.swift
//  mapps
//
//  Created by 太阳在线YHY on 2017/3/1.
//  Copyright © 2017年 太阳在线. All rights reserved.
//

@objc protocol YHYCollectionViewLayoutDelegate {
 //waterFall的列数
 func columnOfWaterFall(_ collectionView: UICollectionView) -> Int
 //每个item的高度
 func waterFall(_ collectionView: UICollectionView, layout waterFallLayout: YHYCollectionViewLayout, heightForItemAt indexPath: IndexPath) -> CGFloat
}

import UIKit

class YHYCollectionViewLayout: UICollectionViewLayout {

 var delegate: YHYCollectionViewLayoutDelegate?
 // 列数 默认是2
 @IBInspectable var columnCount: CGFloat = 2
 // 列间距 默认是0
 @IBInspectable var columnSpacing: CGFloat = 0
 // 行间距 默认是0
 @IBInspectable var lineSpacing: CGFloat = 0
 // section 和 collectionView 的间距 默认是(0,0,0,0)
 @IBInspectable var sectionInsets: UIEdgeInsets = UIEdgeInsets.zero
 // setctionTop
 @IBInspectable var sectionTop: CGFloat = 0 {
  willSet {
   sectionInsets.top = newValue
  }
 }
 
 @IBInspectable var sectionBottom: CGFloat = 0 {
  willSet {
   sectionInsets.bottom = newValue
  }
 }
 
 @IBInspectable var sectionLeft: CGFloat = 0 {
  willSet {
   sectionInsets.left = newValue
  }
 }
 
 @IBInspectable var sectionRight: CGFloat = 0 {
  willSet {
   sectionInsets.right = newValue
  }
 }
 
 // 每行对应的高度
 private var columnHeights: [Int: CGFloat] = [Int: CGFloat]()
 private var attributes: [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
 
 // 自定义初始化方法 (因为这里要定义瀑布流的效果,所以初始化方法设定了  行间距  列间距  和 section之间的设定)
 init(lineSpacing: CGFloat,columnSpacing: CGFloat, sectionInsets: UIEdgeInsets) {
  super.init()
  self.lineSpacing  = lineSpacing
  self.columnSpacing  = columnSpacing
  self.sectionInsets = sectionInsets
 }
 
 required init?(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
 }
 
  // 重写父类方法  子类必须重写此方法,并使用它返回collectionView的内容的宽度和高度。 这些值表示所有内容的宽度和高度,而不仅仅是当前可见的内容。 collectionView使用此信息来配置其自己的内容大小以用于滚动目的。
 override var collectionViewContentSize: CGSize {
  var maxHeight: CGFloat = 0
  for height in columnHeights.values {
   if height > maxHeight {
    maxHeight = height
   }
  }
  return CGSize(width: collectionView?.frame.width ?? 0, height: maxHeight + sectionInsets.bottom)
 }
 
//  重写 prepare方法 ,这个方法必须写,它是用来告诉 layout 要更改当前的布局,也可以在这个方法里做一些准备的工作
 override func prepare() {
  super.prepare()
  guard collectionView != nil else {
   return
  }
  
  if let columnCount = delegate?.columnOfWaterFall(collectionView!) {
   for i in 0..<columnCount {
               columnHeights[i] = sectionInsets.top
   }
  }
  
  let itemCount = collectionView!.numberOfItems(inSection: 0)
  attributes.removeAll()
  for i in 0..<itemCount {
   if let att = layoutAttributesForItem(at: IndexPath.init(row: i, section: 0)) {
    attributes.append(att)
   }
  }
  
 }
 
 // 重写 layoutAttributesForItem 方法 用来计算每个 cell 的大小  子类必须重写此方法,并使用它来返回集合视图中项目的布局信息。 您可以使用此方法仅为具有相应单元格的项目提供布局信息。
 override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  
  if let collectionView = collectionView {
   // 根据 indexPath 获取 item 的 attributes
   let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
   // 获取 collectionView 的宽
   let width = collectionView.frame.width
   
   //
   if let  columnCount = delegate?.columnOfWaterFall(collectionView) {
    //  columnCount 是 collectionView 中 item 的列数
    guard columnCount > 0 else {
     return nil
    }
    
    // item 的宽度 = (colelctionView 的 宽 - 边距 - 列间距)/ 列数
    
    // 每一行总的 item 的宽度
    let totalWidth = (width - sectionInsets.left - sectionInsets.right - (CGFloat(columnCount) - 1) * columnSpacing)
    // 每个 item 的宽度
    let itemWidth = totalWidth / CGFloat(columnCount)
    print(itemWidth)
    // 计算 item 的高度
    let itemheight = delegate?.waterFall(collectionView, layout: self, heightForItemAt: indexPath) ?? 0
    
    // 找出最短的一列
    var minIndex = 0
    for column in columnHeights {
     if column.value < columnHeights[minIndex] ?? 0 {
      minIndex = column.key
        }
        }
    
    // 根据 最短列的列数计算 item 的 x 值
    let itemX = sectionInsets.left + (columnSpacing + itemWidth) * CGFloat(minIndex)
    
    // item 的 y 值 = 最短列的最大+ 行间距
    let itemY = (columnHeights[minIndex] ?? 0) + lineSpacing
    
    // 设置 attributes 的 frame
    attribute.frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: itemheight)
    // 更新字典中的最大 y 值
    columnHeights[minIndex] = attribute.frame.maxY

   }
     return attribute
    }
  return nil
 }
 
//子类必须重写此方法,并使用它返回视图与指定矩形相交的所有项的布局信息。 您的实现应该返回所有可视元素的属性,包括单元格,补充视图和装饰视图。
//创建布局属性时( layout attributes),始终创建表示正确元素类型(单元格,补充或装饰)的属性对象。 集合视图区分每种类型的属性,并使用该信息来决定要创建的视图以及如何管理它们。
 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  return attributes
 }
}

复制代码

如何自定义代码中有很详细的解释,都是通过重写父类的方法来重新构建 collectionView 的布局样式的,具体的布局方式要依靠自己在重写父类方法的时候具体来计算才行。所以思想很简单,关键在于布局的计算方法,想要做出不同的样式就要依靠强大的计算方式来实现。

下面再贴一段环形布局方式的代码:可以研究一下他又是如何来计算出布局的:

//
//  YHYCircleCollectionViewLayout.swift
//  mapps
//
//  Created by 太阳在线YHY on 2017/3/1.
//  Copyright © 2017年 太阳在线. All rights reserved.
//

import UIKit

class YHYCircleCollectionViewLayout: UICollectionViewLayout {

 private var attributes: [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()

 @IBInspectable var center: CGPoint!
 var itemCount: Int!
 var radius: CGFloat!
 
 override func prepare() {
  super.prepare()
  // 总共的 cell
  itemCount = collectionView!.numberOfItems(inSection: 0)
  
  center = CGPoint(x: collectionView!.frame.width / 2, y: collectionView!.frame.height / 2)
  radius = min(collectionView!.frame.width, collectionView!.frame.height) / 4
  attributes.removeAll()
  for i in 0..<itemCount {
   if let att = layoutAttributesForItem(at: IndexPath.init(row: i, section: 0)) {
    attributes.append(att)
   }
  }
 }
 
 override var collectionViewContentSize: CGSize {

  return CGSize(width: collectionView?.frame.width ?? 0, height: (collectionView?.frame.width)!)
 }
 
 override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
  attribute.size = CGSize(width: 60.0, height: 60.0)
  // 当前cell的角度
  // 注意类型转换
  let angle = 2 * CGFloat(M_PI) * CGFloat(indexPath.row) / CGFloat(itemCount)
  // 一点点数学转换
  attribute.center = CGPoint(x: center.x + radius*cos(angle), y: center.y + radius * sin(angle))
  return attribute
 }
 
 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  return attributes
 }
 
}

复制代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值