温馨提示:本文最后更新于
2024-10-23 18:32:53
,某些文章具有时效性,若有错误或已失效,请在下方留言。Layout
协议,可以创建⾃定义的容器视图,让它们根据所编写的算法来布局⼦视图。这个协议的使用分为两步:
- ⾸先,使⽤
sizeThatFits
⽅法确定容器的尺⼨。在该⽅法内部,我们通过⼦视图的代理,来访问这些⼦视图。视图代理允许我们通过向⼦视图提供不同尺⼨的建议来测量⼦视图。 - 将⼦视图放置在容器的尺⼨内。同样,我们可以通过⼦视图的代理访问这些⼦视图。
本文以流式布局,使⽤ Layout
协议,我们将按照以下步骤实现流式布局:
- 通过向每个⼦视图建议 nil⨉nil 尺⼨,来询问它们的理想尺⼨。
- 根据容器⾃身的尺⼨,计算所有⼦视图的位置。
- 基于步骤 2 的结果,将每个⼦视图放在计算出的位置上
步骤 2 中的基于⾏的布局算法的参数包含尺⼨列表 (其中是步骤 1 得到的各个理想尺⼨)、间距和容器宽度。它返回⼀个 frame 的数组 (其中每个元素对于着⼀个输⼊尺⼨)。它的⼯作⽅式如下:
- 我们使⽤⼀个初始位置为 (0, 0) 的 CGPoint 来追踪当前位置。其中 x 分量是当前⾏内的⽔平位置,每添加⼀个视图都会改变。y 分量是当前⾏的顶部,它仅在开始新的⼀⾏时才会改变。
- 我们遍历所有的⼦视图。
- 如果⼦视图⽆法适应当前⾏,我们通过将当前位置的 x 分量重置为零并将 y 分量增加当前⾏的⾼度加上间距来开始新的⼀⾏。
- 我们将当前位置⽤作此⼦视图矩形的原点。
- 我们将当前位置的 x 分量增加⼦视图的宽度并添加间距。
func layout(
sizes: [CGSize],
spacing: CGSize = .init(width: 10, height: 10),
containerWidth: CGFloat
) -> [CGRect] {
var result: [CGRect] = []
var currentPosition: CGPoint = .zero
func startNewline() {
if currentPosition.x == 0 {return}
currentPosition.x = 0
currentPosition.y = result.union().maxY + spacing.height
}
for size in sizes {
if currentPosition.x + size.width > containerWidth {
startNewline()
}
result.append(CGRect(origin: currentPosition, size: size))
currentPosition.x += size.width + spacing.width
}
return result
}
完整代码
extension Array where Element == CGRect {
func union() -> CGRect {
self.reduce(.zero) { partialResult, rect in
partialResult.union(rect)
}
}
}
extension Color {
static func random() -> Color {
Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1))
}
}
struct Flowlayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let containerWidth = proposal.replacingUnspecifiedDimensions().width
let sizes = subviews.map {
$0.sizeThatFits(.unspecified)
}
return layout(sizes: sizes, containerWidth: containerWidth).union().size
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let subViews = subviews.map {
$0.sizeThatFits(.unspecified)
}
let frames = layout(sizes: subViews, containerWidth: bounds.width)
for(f, subview) in zip(frames, subviews) {
let offset = CGPoint(x: f.origin.x + bounds.minX, y: f.origin.y + bounds.minY)
subview.place(at: offset, proposal: .unspecified)
}
}
func layout(
sizes: [CGSize],
spacing: CGSize = .init(width: 10, height: 10),
containerWidth: CGFloat
) -> [CGRect] {
var result: [CGRect] = []
var currentPosition: CGPoint = .zero
func startNewline() {
if currentPosition.x == 0 {return}
currentPosition.x = 0
currentPosition.y = result.union().maxY + spacing.height
}
for size in sizes {
if currentPosition.x + size.width > containerWidth {
startNewline()
}
result.append(CGRect(origin: currentPosition, size: size))
currentPosition.x += size.width + spacing.width
}
return result
}
}
struct ContentView: View {
var body: some View {
Flowlayout {
ForEach(Array(0...10), id: \.self) { idx in
Rectangle()
.fill(Color.random())
.frame(width: .random(in: 100...350), height: 120)
}
}
.border(.blue)
.border(.black)
}
}
流式布局的效果图,如下所示

运行效果
参考 《Thinking In SwiftUI》
© 版权声明
THE END
暂无评论内容