feat: 圆形和三角形组件
This commit is contained in:
parent
62dbd2594c
commit
e74040a444
99
wake/SharedUI/Graphics/Circle.swift
Normal file
99
wake/SharedUI/Graphics/Circle.swift
Normal 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
|
||||||
202
wake/SharedUI/Graphics/Triangle.swift
Normal file
202
wake/SharedUI/Graphics/Triangle.swift
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user