186 lines
6.0 KiB
Swift
186 lines
6.0 KiB
Swift
//
|
||
// FlowContainer.swift
|
||
// Crush
|
||
//
|
||
// Created by Leon on 2025/7/19.
|
||
//
|
||
|
||
import UIKit
|
||
|
||
class FlowAutoLayoutContainer: UIView {
|
||
// MARK: - Properties
|
||
|
||
/// Item 之间的水平间距
|
||
var itemSpacing: CGFloat = 8.0 {
|
||
didSet { setNeedsLayout() }
|
||
}
|
||
|
||
/// 行之间的垂直间距
|
||
var lineSpacing: CGFloat = 8.0 {
|
||
didSet { setNeedsLayout() }
|
||
}
|
||
|
||
/// 存储子视图
|
||
private var arrangedSubviews: [UIView] = []
|
||
|
||
// MARK: - Initialization
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
setup()
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
super.init(coder: coder)
|
||
setup()
|
||
}
|
||
|
||
private func setup() {
|
||
// 设置默认背景色
|
||
backgroundColor = .clear
|
||
}
|
||
|
||
// MARK: - Public Methods
|
||
|
||
/// 添加单个子视图
|
||
func addArrangedSubview(_ view: UIView) {
|
||
arrangedSubviews.append(view)
|
||
addSubview(view)
|
||
setNeedsLayout()
|
||
}
|
||
|
||
/// 添加多个子视图
|
||
func addArrangedSubviews(_ views: [UIView]) {
|
||
arrangedSubviews.append(contentsOf: views)
|
||
views.forEach { addSubview($0) }
|
||
setNeedsLayout()
|
||
}
|
||
|
||
/// 清空所有子视图
|
||
func removeAllArrangedSubviews() {
|
||
arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||
arrangedSubviews.removeAll()
|
||
setNeedsLayout()
|
||
}
|
||
|
||
// MARK: - Layout
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
|
||
// 清空现有约束
|
||
arrangedSubviews.forEach { $0.snp.removeConstraints() }
|
||
|
||
// 当前布局参数
|
||
let containerWidth = bounds.width
|
||
var currentX: CGFloat = 0
|
||
var currentY: CGFloat = 0
|
||
var currentRowHeight: CGFloat = 0
|
||
var currentRowViews: [UIView] = [] // 当前行的所有views
|
||
var previousView: UIView?
|
||
|
||
// 遍历子视图进行布局
|
||
for view in arrangedSubviews {
|
||
view.translatesAutoresizingMaskIntoConstraints = false
|
||
let viewSize = view.intrinsicContentSize == .zero ? view.bounds.size : view.intrinsicContentSize
|
||
|
||
// 检查是否需要换行
|
||
if currentX + viewSize.width > containerWidth && !currentRowViews.isEmpty {
|
||
// 布局当前行(底部对齐)
|
||
layoutRow(views: currentRowViews, rowHeight: currentRowHeight, startY: currentY, previousView: &previousView)
|
||
|
||
// 重置参数开始新行
|
||
currentX = 0
|
||
currentY += currentRowHeight + lineSpacing
|
||
currentRowHeight = 0
|
||
currentRowViews.removeAll()
|
||
}
|
||
|
||
// 更新当前行信息
|
||
currentRowViews.append(view)
|
||
currentX += viewSize.width + itemSpacing
|
||
currentRowHeight = max(currentRowHeight, viewSize.height)
|
||
}
|
||
|
||
// 布局最后一行
|
||
if currentRowViews.isEmpty == false { // has views in lastViews
|
||
layoutRow(views: currentRowViews, rowHeight: currentRowHeight, startY: currentY, previousView: &previousView)
|
||
if let last = currentRowViews.last {
|
||
//dlog("💤currentY:\(currentY)")
|
||
last.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(currentY)
|
||
}
|
||
}
|
||
invalidateIntrinsicContentSize() // 🔥重要
|
||
}
|
||
|
||
//dlog("💤currentRowHeight \(currentRowHeight)")
|
||
bounds = CGRect(x: 0, y: 0, width: intrinsicContentSize.width, height: intrinsicContentSize.height)
|
||
//dlog("💤currentRowHeight:\(currentRowHeight) intrinsicContentSize:\(intrinsicContentSize) sizeHeight:\(bounds.size.height)")
|
||
}
|
||
|
||
/// 布局一行中的视图,底部对齐
|
||
private func layoutRow(views: [UIView], rowHeight: CGFloat, startY: CGFloat, previousView: inout UIView?) {
|
||
for (index, view) in views.enumerated() {
|
||
view.snp.makeConstraints { make in
|
||
// 底部对齐:bottom = startY + rowHeight
|
||
make.bottom.equalTo(snp.top).offset(startY + rowHeight)
|
||
|
||
if index == 0 {
|
||
// 每行第一个视图,靠容器左边
|
||
make.leading.equalToSuperview()
|
||
} else if let prev = previousView {
|
||
// 非第一个视图,紧接前一个视图
|
||
make.leading.equalTo(prev.snp.trailing).offset(itemSpacing)
|
||
}
|
||
|
||
// 高度基于 intrinsicContentSize
|
||
if view.intrinsicContentSize.height != UIView.noIntrinsicMetric {
|
||
make.height.equalTo(view.intrinsicContentSize.height)
|
||
}
|
||
}
|
||
previousView = view
|
||
}
|
||
}
|
||
|
||
/// 计算容器所需的高度
|
||
override var intrinsicContentSize: CGSize {
|
||
// layoutIfNeeded()
|
||
//
|
||
// var totalHeight: CGFloat = 0
|
||
// var currentX: CGFloat = 0
|
||
// var currentRowHeight: CGFloat = 0
|
||
//
|
||
// for view in arrangedSubviews {
|
||
// let viewSize = view.intrinsicContentSize == .zero ? view.bounds.size : view.intrinsicContentSize
|
||
//
|
||
// if currentX + viewSize.width > bounds.width && currentX > 0 {
|
||
// totalHeight += currentRowHeight + lineSpacing
|
||
// currentX = 0
|
||
// currentRowHeight = 0
|
||
// }
|
||
//
|
||
// currentX += viewSize.width + itemSpacing
|
||
// currentRowHeight = max(currentRowHeight, viewSize.height)
|
||
// }
|
||
//
|
||
// // 加上最后一行的行高
|
||
// if currentRowHeight > 0 {
|
||
// totalHeight += currentRowHeight
|
||
// }
|
||
//
|
||
// dlog("💤Totalheight: \(totalHeight)")
|
||
// return CGSize(width: bounds.width, height: totalHeight)
|
||
|
||
layoutIfNeeded()
|
||
|
||
if subviews.count > 0{
|
||
let view = subviews.last!
|
||
// dlog("💤 last:\(view)")
|
||
let height = max(view.frame.maxY, 30)
|
||
return CGSize(width: bounds.width, height: height)
|
||
}
|
||
return CGSize(width: bounds.width, height: 30)
|
||
}
|
||
}
|