UICollectionView 自定義樣式的實現 Swift3.0 (瀑布流等佈局)

UICollectionView 是在 iOS6 時出現的,不少人都會把它和 UITableView 作一個比較,相比而言UICollectionView 要更強大、同時要比 UITableView 的佈局結構更加靈活徹底。像瀑布流這樣的靈活佈局就是用 UICollectionView 來實現的。這篇文章咱們主要就來了解一下 UICollectionView 是怎樣實現這樣的靈活佈局的。swift

UICollectionView 的靈活主要得益於UICollectionViewLayout,那麼 UICollectionViewLayout 是什麼呢?bash

UICollectionViewLayout 簡介

UICollectionViewLayout 是什麼呢?咱們首先 來看一下 官方 API 給出的解釋:app

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.ide

大體的意思是:UICollectionViewLayout類是一個抽象類,並使用生成UICollectionView的佈局信息。他佈局時候的工做是來肯定 Cell 的位置的設置等一些佈局的信息。而後UICollectionView提供的佈局信息適用於相應的視圖,這樣他們就能夠呈如今屏幕上。佈局

總而言之一句話就是: UICollectionViewLayout 決定了 CollectionView 的佈局樣式(cell 的排列方式)ui

UICollectionViewLayout的使用

既然咱們已經知道了 UICollectionView 的靈活佈局全靠UICollectionViewLayout,那麼應該怎麼使用UICollectionViewLayout就能夠作出瀑布流這樣的佈局呢?spa

首先必須寫一個UICollectionViewLayout的子類,並重寫UICollectionViewLayout的方法,咱們以寫瀑布流爲例:看一下示例代碼code

//
//  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 的佈局樣式的,具體的佈局方式要依靠本身在重寫父類方法的時候具體來計算才行。因此思想很簡單,關鍵在於佈局的計算方法,想要作出不一樣的樣式就要依靠強大的計算方式來實現。orm

下面再貼一段環形佈局方式的代碼:能夠研究一下他又是如何來計算出佈局的:cdn

//
//  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
 }
 
}

複製代碼

瀑布流示意圖.png
環形示意圖.png
相關文章
相關標籤/搜索