import SwiftUI /// 三角形方向 public enum TriangleDirection: Equatable { case up case down case left case right } /// 一个可插入(inset)的三角形 Shape,支持四个方向。 struct TriangleShape: InsettableShape { var direction: TriangleDirection = .up var insetAmount: CGFloat = 0 var animatableData: CGFloat { get { insetAmount } set { insetAmount = newValue } } func inset(by amount: CGFloat) -> TriangleShape { var copy = self copy.insetAmount += amount return copy } func path(in rect: CGRect) -> Path { let r = rect.insetBy(dx: insetAmount, dy: insetAmount) // 顶点布局:根据方向决定三点位置 let p1: CGPoint let p2: CGPoint let p3: CGPoint switch direction { case .up: p1 = CGPoint(x: r.midX, y: r.minY) p2 = CGPoint(x: r.maxX, y: r.maxY) p3 = CGPoint(x: r.minX, y: r.maxY) case .down: p1 = CGPoint(x: r.midX, y: r.maxY) p2 = CGPoint(x: r.minX, y: r.minY) p3 = CGPoint(x: r.maxX, y: r.minY) case .left: p1 = CGPoint(x: r.minX, y: r.midY) p2 = CGPoint(x: r.maxX, y: r.minY) p3 = CGPoint(x: r.maxX, y: r.maxY) case .right: p1 = CGPoint(x: r.maxX, y: r.midY) p2 = CGPoint(x: r.minX, y: r.maxY) p3 = CGPoint(x: r.minX, y: r.minY) } var path = Path() path.move(to: p1) path.addLine(to: p2) path.addLine(to: p3) path.closeSubpath() return path } } /// 可配置的三角形视图封装,便于在项目中复用。 /// 示例: /// TriangleView(width: 20, height: 18, direction: .up, color: .black, stroke: .white, lineWidth: 2) public struct TriangleView: View { public var width: CGFloat public var height: CGFloat public var direction: TriangleDirection public var color: Color public var stroke: Color? = nil public var lineWidth: CGFloat = 1 public var rotation: Angle = .degrees(0) public init( width: CGFloat, height: CGFloat, direction: TriangleDirection = .up, color: Color = .black, stroke: Color? = nil, lineWidth: CGFloat = 1, rotation: Angle = .degrees(0) ) { self.width = width self.height = height self.direction = direction self.color = color self.stroke = stroke self.lineWidth = lineWidth self.rotation = rotation } public var body: some View { let shape = TriangleShape(direction: direction) Group { if let stroke = stroke, lineWidth > 0 { shape.fill(color) .overlay( shape.stroke(stroke, lineWidth: lineWidth) ) } else { shape.fill(color) } } .frame(width: width, height: height) .rotationEffect(rotation) .accessibilityHidden(true) } } /// 水平渲染多个等尺寸三角形的小组件。 public struct TriangleRow: View { public var count: Int public var itemSize: CGSize public var direction: TriangleDirection public var color: Color public var spacing: CGFloat public var rotation: Angle public init( count: Int, itemSize: CGSize, direction: TriangleDirection = .up, color: Color = .black, spacing: CGFloat = 8, rotation: Angle = .degrees(0) ) { self.count = count self.itemSize = itemSize self.direction = direction self.color = color self.spacing = spacing self.rotation = rotation } public var body: some View { HStack(spacing: spacing) { ForEach(0..<(max(0, count)), id: \.self) { _ in TriangleView( width: itemSize.width, height: itemSize.height, direction: direction, color: color, rotation: rotation ) } } .accessibilityElement(children: .ignore) } } #if DEBUG struct Triangle_Previews: PreviewProvider { static var previews: some View { Group { // 单个三角形 TriangleView(width: 24, height: 20, direction: .up, color: .black, stroke: .white, lineWidth: 2) .padding() .previewDisplayName("Single Up") TriangleView(width: 24, height: 20, direction: .right, color: .orange) .padding() .background(Color(.systemGroupedBackground)) .previewDisplayName("Single Right") TriangleView(width: 24, height: 20, direction: .up, color: .blue, rotation: .degrees(132)) .padding() .previewDisplayName("Rotated 132") // 行展示 ZStack { Color(white: 0.97) HStack { RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(.white) .shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 2) .overlay( HStack { TriangleRow( count: 6, itemSize: CGSize(width: 16, height: 14), direction: .up, color: .black, spacing: 6, rotation: .degrees(180) ) } .padding(.horizontal, 18) ) .frame(height: 64) .padding() } } .previewDisplayName("Row") } .previewLayout(.sizeThatFits) } } #endif