SwiftUI中如何用自定义形状创建六边形镶嵌(蜂窝网格)
使用SwiftUI的Path实现六边形镶嵌效果
我已经用Path创建了六边形Shape,但不知道怎么实现类似下图的镶嵌效果:
struct Hexagon: Shape { func path(in rect: CGRect) -> Path { var path = Path() let center = CGPoint(x: rect.midX, y: rect.midY) let size = min(rect.size.width, rect.size.height) / 2 let angleOffset = CGFloat.pi / 6 for i in 0..<6 { let angle = CGFloat(i) * CGFloat.pi / 3 + angleOffset let point = CGPoint(x: center.x + cos(angle) * size, y: center.y + sin(angle) * size) if i == 0 { path.move(to: point) } else { path.addLine(to: point) } } path.closeSubpath() return path } }
期望实现的效果:
实现方案
核心几何逻辑
六边形能无缝镶嵌的关键在于:
- 六边形对边平行且长度相等,相邻行的六边形需要偏移半个宽度
- 垂直方向上,相邻行的间距是六边形高度的3/4(上下行六边形会重叠1/4的高度)
方法一:用LazyVGrid快速实现
调整六边形的尺寸适配,配合行偏移即可快速得到镶嵌效果:
// 调整后的六边形Shape,保证宽高比符合镶嵌要求 struct Hexagon: Shape { func path(in rect: CGRect) -> Path { var path = Path() let center = CGPoint(x: rect.midX, y: rect.midY) // 以宽度为基准计算边长,确保六边形宽高比为2:√3 let sideLength = rect.width / 2 let size = sideLength let angleOffset = CGFloat.pi / 6 for i in 0..<6 { let angle = CGFloat(i) * CGFloat.pi / 3 + angleOffset let point = CGPoint(x: center.x + cos(angle) * size, y: center.y + sin(angle) * size) if i == 0 { path.move(to: point) } else { path.addLine(to: point) } } path.closeSubpath() return path } } // 镶嵌视图 struct HexTessellationView: View { // 单个六边形的宽度 private let hexWidth: CGFloat = 60 // 自适应列数,根据屏幕宽度自动调整 private let columns = [GridItem(.adaptive(minimum: 60))] var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: hexWidth * sqrt(3)/2 - hexWidth/2) { ForEach(0..<60) { index in Hexagon() .fill(Color.blue.opacity(0.7)) .stroke(Color.black, lineWidth: 1) .frame(width: hexWidth, height: hexWidth * sqrt(3)) // 奇数行的六边形偏移半个宽度 .offset(x: (index / columns.count) % 2 == 1 ? hexWidth/2 : 0) } } .padding() } } }
方法二:自定义Layout实现精准控制
如果需要固定列数、动态调整位置等灵活需求,可使用自定义Layout:
struct HexLayout: Layout { // 固定列数,可按需修改 let columnCount: Int func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { guard !subviews.isEmpty else { return .zero } let subviewSize = subviews[0].sizeThatFits(.unspecified) let hexWidth = subviewSize.width let hexHeight = subviewSize.height let rowCount = (subviews.count + columnCount - 1) / columnCount // 总宽度包含奇数行偏移的额外宽度 let totalWidth = CGFloat(columnCount) * hexWidth + hexWidth/2 // 总高度按行间距为3/4六边形高度计算 let totalHeight = CGFloat(rowCount) * hexHeight * 0.75 + hexHeight * 0.25 return CGSize(width: totalWidth, height: totalHeight) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { guard !subviews.isEmpty else { return } let subviewSize = subviews[0].sizeThatFits(.unspecified) let hexWidth = subviewSize.width let hexHeight = subviewSize.height var currentX = bounds.minX + hexWidth/2 var currentY = bounds.minY + hexHeight/4 for (index, subview) in subviews.enumerated() { let rowIndex = index / columnCount // 奇数行偏移半个宽度 let xOffset = rowIndex % 2 == 1 ? hexWidth/2 : 0 subview.place(at: CGPoint(x: currentX + xOffset, y: currentY), proposal: proposal) currentX += hexWidth // 换行时重置X坐标,调整Y坐标 if (index + 1) % columnCount == 0 { currentX = bounds.minX + hexWidth/2 currentY += hexHeight * 0.75 } } } } // 使用自定义Layout的镶嵌视图 struct HexTessellationView: View { private let hexWidth: CGFloat = 60 var body: some View { ScrollView { HexLayout(columnCount: 5) { ForEach(0..<50) { _ in Hexagon() .fill(Color.blue.opacity(0.7)) .stroke(Color.black, lineWidth: 1) .frame(width: hexWidth, height: hexWidth * sqrt(3)) } } .padding() } } }
内容的提问来源于stack exchange,提问作者Yasha
相关产品推荐
相关产品推荐

