202 lines
6.0 KiB
Swift
202 lines
6.0 KiB
Swift
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 |