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