Last active
April 30, 2020 03:32
-
-
Save AmatsuZero/4a0f12a1d82f34dc6dd7c5c842e0559a to your computer and use it in GitHub Desktop.
iOS不规则瀑布流
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
public protocol RandomWaterfallLayoutDelegate: class { | |
/// 列数 | |
func columnCount(_ layout: RandomWaterfallLayout) -> CGFloat | |
/// 列间距 | |
func columnMargin(_ layout: RandomWaterfallLayout) -> CGFloat | |
/// 行间距 | |
func rowMargin(_ layout: RandomWaterfallLayout) -> CGFloat | |
/// CollectionView 边距 | |
func edgeInsets(_ layout: RandomWaterfallLayout) -> UIEdgeInsets | |
/// 返回Cell 尺寸 | |
func cellSize(at indexPath: IndexPath) -> CGSize | |
} | |
public class RandomWaterfallLayout: UICollectionViewLayout { | |
public var pageCount: Int { | |
return 48 | |
} | |
public weak var delegate: RandomWaterfallLayoutDelegate? | |
/// 所有的cell的布局 | |
private var attrsArray = [UICollectionViewLayoutAttributes]() | |
/// 每一列的高度 | |
private var columnHeights = [CGFloat]() | |
/// 没有生成大尺寸次数 | |
private var noneDouleTime = 0 | |
/// 最后一次大尺寸的列数 | |
private var lastDoubleIndex = 0 | |
/// 默认列数 | |
private let defaultColumnCount: CGFloat = 3 | |
/// 默认列边距 | |
private let defaultColumMargin: CGFloat = 10 | |
/// 默认行边距 | |
private let defaultRowMargin: CGFloat = 10 | |
/// 默认collectionView边距 | |
private let defaultUIEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) | |
/// 列数 | |
private var columnCount: CGFloat { | |
return delegate?.columnCount(self) ?? defaultColumnCount | |
} | |
/// 列边距 | |
private var columnMargin: CGFloat { | |
return delegate?.rowMargin(self) ?? defaultRowMargin | |
} | |
/// 行边距 | |
private var rowMargin: CGFloat { | |
return delegate?.rowMargin(self) ?? defaultRowMargin | |
} | |
// collectionView边距 | |
private var edgeInsets: UIEdgeInsets { | |
return delegate?.edgeInsets(self) ?? defaultUIEdgeInsets | |
} | |
// collectionView 首次布局和之后重新布局的时候会调用 | |
// 并不是每次滑动都调用,只有在数据源变化的时候才调用 | |
public override func prepare() { | |
// 重写必须调用super方法 | |
super.prepare() | |
guard let collectionView = collectionView else { | |
return | |
} | |
if collectionView.numberOfItems(inSection: 0) == pageCount, attrsArray.count > pageCount { | |
attrsArray.removeAll() | |
columnHeights.removeAll() | |
} | |
// 当列高度数组为空时,即为第一行计算,每一列的基础高度加上collection的边框的top值 | |
if columnHeights.isEmpty { | |
columnHeights.append(contentsOf: [CGFloat](repeating: edgeInsets.top, count: Int(columnCount))) | |
} | |
// 遍历所有的cell,计算所有cell的布局 | |
for i in attrsArray.count..<collectionView.numberOfItems(inSection: 0) { | |
if let attr = layoutAttributesForItem(at: .init(row: i, section: 0)) { | |
attrsArray.append(attr) | |
} | |
} | |
} | |
// 返回布局属性,一个UICollectionViewLayoutAttributes对象数组 | |
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { | |
return attrsArray | |
} | |
public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { | |
guard let collectionView = self.collectionView, | |
let size = delegate?.cellSize(at: indexPath), | |
!columnHeights.isEmpty else { | |
return nil | |
} | |
let attrs = UICollectionViewLayoutAttributes(forCellWith: indexPath) | |
// cell的宽度 | |
let w = collectionView.frame.width - edgeInsets.left - edgeInsets.right - columnMargin * (columnCount - 1) / columnCount | |
// cell的高度 | |
let h = size.width / size.height * w | |
// 高度最小的列数高度 | |
let minColumnHeight = columnHeights.sorted().first! | |
// 获取高度最小的列数 | |
// cell应该拼接的列数 | |
let destColumn = columnHeights.firstIndex(of: minColumnHeight)! | |
// 计算cell的x | |
let x = edgeInsets.left + CGFloat(destColumn) * (w + columnMargin) | |
// 计算cell的y | |
let y = minColumnHeight != edgeInsets.top ? minColumnHeight + edgeInsets.top : minColumnHeight | |
// 判断是否放大 | |
if CGFloat(destColumn) < columnCount - 1, // 放大的列数不能是最后一列(最后一列方法超出屏幕) | |
noneDouleTime > 1, // 如果前个cell有放大就不放大,防止连续出现两个放大 | |
Int.random(in: 0..<100) > 33, // 33%几率不放大 | |
columnHeights[destColumn] == columnHeights[destColumn + 1], // 当前列的顶部和下一列的顶部要对齐 | |
lastDoubleIndex != destColumn {// 最后一次放大的列不等当前列,防止出现连续两列出现放大不美观 | |
noneDouleTime = 0 | |
lastDoubleIndex = destColumn | |
// 重定义当前cell的布局:宽度*2,高度*2 | |
attrs.frame.origin = CGPoint(x: x, y: y) | |
attrs.frame.size.width = w * 2 + columnMargin | |
attrs.frame.size.height = h * 2 + rowMargin | |
columnHeights[destColumn] = attrs.frame.maxY | |
columnHeights[destColumn+1] = attrs.frame.maxY | |
} else { | |
// 正常cell的布局 | |
if columnHeights.count > destColumn + 1, abs(y + h - columnHeights[destColumn + 1]) < h * 0.2 { | |
// 当前cell填充后和上一列的高度偏差不超过cell最大高度的10%,就和下一列对齐 | |
attrs.frame = CGRect(x: x, y: y, width: w, height: columnHeights[destColumn+1] - y) | |
} else if destColumn >= 1, abs(y + h - columnHeights[destColumn - 1]) < h * 0.2 { | |
// 当前cell填充后和上上列的高度偏差不超过cell最大高度的10%,就和下一列对齐 | |
attrs.frame = CGRect(x: x, y: y, width: w, height: columnHeights[destColumn - 1] - y) | |
} else { | |
attrs.frame = CGRect(x: x, y: y, width: w, height: h) | |
} | |
// 当前cell列的高度就是当前cell的最大Y值 | |
columnHeights[destColumn] = attrs.frame.maxY | |
noneDouleTime += 1 | |
} | |
return attrs | |
} | |
public override var collectionViewContentSize: CGSize { | |
guard !columnHeights.isEmpty else { | |
return .zero | |
} | |
// collectionView的contentSize的高度等于所有列高度中最大的值 | |
let maxColumnHeight = columnHeights.sorted().last! | |
return CGSize(width: 0, height: maxColumnHeight + edgeInsets.bottom) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment