布局(六) – Layout 协议

温馨提示:本文最后更新于2024-10-23 18:32:53,某些文章具有时效性,若有错误或已失效,请在下方留言

Layout 协议,可以创建⾃定义的容器视图,让它们根据所编写的算法来布局⼦视图。这个协议的使用分为两步:

  1. ⾸先,使⽤ sizeThatFits ⽅法确定容器的尺⼨。在该⽅法内部,我们通过⼦视图的代理,来访问这些⼦视图。视图代理允许我们通过向⼦视图提供不同尺⼨的建议来测量⼦视图。
  2. 将⼦视图放置在容器的尺⼨内。同样,我们可以通过⼦视图的代理访问这些⼦视图。

本文以流式布局,使⽤ Layout 协议,我们将按照以下步骤实现流式布局:

  1. 通过向每个⼦视图建议 nil⨉nil 尺⼨,来询问它们的理想尺⼨
  2. 根据容器⾃身的尺⼨,计算所有⼦视图的位置。
  3. 基于步骤 2 的结果,将每个⼦视图放在计算出的位置上

步骤 2 中的基于⾏的布局算法的参数包含尺⼨列表 (其中是步骤 1 得到的各个理想尺⼨)、间距和容器宽度。它返回⼀个 frame 的数组 (其中每个元素对于着⼀个输⼊尺⼨)。它的⼯作⽅式如下:

  1. 我们使⽤⼀个初始位置为 (0, 0) 的 CGPoint 来追踪当前位置。其中 x 分量是当前⾏内的⽔平位置,每添加⼀个视图都会改变。y 分量是当前⾏的顶部,它仅在开始新的⼀⾏时才会改变。
  2. 我们遍历所有的⼦视图。
    1. 如果⼦视图⽆法适应当前⾏,我们通过将当前位置的 x 分量重置为零并将 y 分量增加当前⾏的⾼度加上间距来开始新的⼀⾏。
    2. 我们将当前位置⽤作此⼦视图矩形的原点。
    3. 我们将当前位置的 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
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容