feat: 圆形和三角形组件

This commit is contained in:
Junhui Chen 2025-09-10 20:16:44 +08:00
parent 62dbd2594c
commit e74040a444
2 changed files with 301 additions and 0 deletions

View File

@ -0,0 +1,99 @@
import SwiftUI
/// 便
/// 使
/// CircleView(diameter: 24, color: .orange, stroke: .white, lineWidth: 2)
public struct CircleView: View {
public var diameter: CGFloat
public var color: Color
public var stroke: Color? = nil
public var lineWidth: CGFloat = 1
public init(
diameter: CGFloat,
color: Color = .black,
stroke: Color? = nil,
lineWidth: CGFloat = 1
) {
self.diameter = diameter
self.color = color
self.stroke = stroke
self.lineWidth = lineWidth
}
public var body: some View {
Group {
if let stroke = stroke, lineWidth > 0 {
Circle()
.fill(color)
.overlay(
Circle().stroke(stroke, lineWidth: lineWidth)
)
} else {
Circle().fill(color)
}
}
.frame(width: diameter, height: diameter)
.accessibilityHidden(true)
}
}
///
public struct CircleRow: View {
public var count: Int
public var diameter: CGFloat
public var color: Color
public var spacing: CGFloat
public init(
count: Int,
diameter: CGFloat,
color: Color = .black,
spacing: CGFloat = 8
) {
self.count = count
self.diameter = diameter
self.color = color
self.spacing = spacing
}
public var body: some View {
HStack(spacing: spacing) {
ForEach(0..<(max(0, count)), id: \.self) { _ in
CircleView(diameter: diameter, color: color)
}
}
.accessibilityElement(children: .ignore)
}
}
#if DEBUG
struct CircleView_Previews: PreviewProvider {
static var previews: some View {
Group {
CircleView(diameter: 40, color: .black, stroke: .white, lineWidth: 2)
.padding()
.previewDisplayName("Single Circle")
ZStack {
Color(.black)
HStack {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(.white)
.shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 2)
.overlay(
HStack {
CircleRow(count: 6, diameter: 10, color: .black, spacing: 6)
}
.padding(.horizontal, 16)
)
.frame(height: 56)
.padding()
}
}
.previewDisplayName("Circle Row")
}
.previewLayout(.sizeThatFits)
}
}
#endif

View File

@ -0,0 +1,202 @@
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