Compare commits
No commits in common. "e74040a444735c6859133648ef7e22cd81cd8b3f" and "ff31361867b41f336425781787e646eb1ef18f62" have entirely different histories.
e74040a444
...
ff31361867
@ -1,99 +0,0 @@
|
||||
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
|
||||
@ -1,174 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
/// A skewed rounded rectangle rendered via shearing a RoundedRectangle.
|
||||
/// - Parameters:
|
||||
/// - shear: Horizontal shear factor. Positive values lean to the right. Typical 0.15 ~ 0.35
|
||||
/// - cornerRadius: Corner radius of the base rounded rectangle before shear.
|
||||
struct ParallelogramShape: InsettableShape {
|
||||
var shear: CGFloat = 0.25
|
||||
var cornerRadius: CGFloat = 6
|
||||
var insetAmount: CGFloat = 0
|
||||
|
||||
var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
||||
get { AnimatablePair(shear, cornerRadius) }
|
||||
set {
|
||||
shear = newValue.first
|
||||
cornerRadius = newValue.second
|
||||
}
|
||||
}
|
||||
|
||||
func inset(by amount: CGFloat) -> ParallelogramShape {
|
||||
var copy = self
|
||||
copy.insetAmount += amount
|
||||
return copy
|
||||
}
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
// To keep the final shape inside `rect`, we draw a rounded rectangle inset by half of the shear expansion
|
||||
// and apply a shear transform around the vertical center.
|
||||
let h = rect.height
|
||||
let expandX = abs(shear) * h // total width expansion after shearing around center
|
||||
let insetX = expandX / 2 + insetAmount
|
||||
let insetRect = rect.insetBy(dx: insetX, dy: insetAmount)
|
||||
|
||||
let rr = RoundedRectangle(cornerRadius: max(0, cornerRadius - insetAmount), style: .continuous)
|
||||
var path = rr.path(in: insetRect)
|
||||
|
||||
// Transform: translate to center, shear, translate back
|
||||
let toCenter = CGAffineTransform(translationX: 0, y: -rect.midY)
|
||||
let shearTransform = CGAffineTransform(a: 1, b: 0, c: shear, d: 1, tx: 0, ty: 0)
|
||||
let back = CGAffineTransform(translationX: 0, y: rect.midY)
|
||||
path = path.applying(toCenter).applying(shearTransform).applying(back)
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
/// A configurable parallelogram view.
|
||||
/// Example:
|
||||
/// ParallelogramView(width: 36, height: 18, shear: 0.3, cornerRadius: 5, color: .black)
|
||||
public struct ParallelogramView: View {
|
||||
public var width: CGFloat
|
||||
public var height: CGFloat
|
||||
public var shear: CGFloat
|
||||
public var cornerRadius: CGFloat
|
||||
public var color: Color
|
||||
public var stroke: Color? = nil
|
||||
public var lineWidth: CGFloat = 1
|
||||
|
||||
public init(
|
||||
width: CGFloat,
|
||||
height: CGFloat,
|
||||
shear: CGFloat = 0.25,
|
||||
cornerRadius: CGFloat = 6,
|
||||
color: Color = .black,
|
||||
stroke: Color? = nil,
|
||||
lineWidth: CGFloat = 1
|
||||
) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.shear = shear
|
||||
self.cornerRadius = cornerRadius
|
||||
self.color = color
|
||||
self.stroke = stroke
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
let shape = ParallelogramShape(shear: shear, cornerRadius: cornerRadius)
|
||||
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)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal row that renders a given number of parallelograms.
|
||||
public struct ParallelogramRow: View {
|
||||
public var count: Int
|
||||
public var itemSize: CGSize
|
||||
public var shear: CGFloat
|
||||
public var cornerRadius: CGFloat
|
||||
public var color: Color
|
||||
public var spacing: CGFloat
|
||||
|
||||
public init(
|
||||
count: Int,
|
||||
itemSize: CGSize,
|
||||
shear: CGFloat = 0.25,
|
||||
cornerRadius: CGFloat = 6,
|
||||
color: Color = .black,
|
||||
spacing: CGFloat = 8
|
||||
) {
|
||||
self.count = count
|
||||
self.itemSize = itemSize
|
||||
self.shear = shear
|
||||
self.cornerRadius = cornerRadius
|
||||
self.color = color
|
||||
self.spacing = spacing
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack(spacing: spacing) {
|
||||
ForEach(0..<(max(0, count)), id: \.self) { _ in
|
||||
ParallelogramView(
|
||||
width: itemSize.width,
|
||||
height: itemSize.height,
|
||||
shear: shear,
|
||||
cornerRadius: cornerRadius,
|
||||
color: color
|
||||
)
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct Parallelogram_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
// Single parallelogram preview
|
||||
ParallelogramView(width: 20, height: 18, shear: -0.3, cornerRadius: 2, color: .black)
|
||||
.padding()
|
||||
.previewDisplayName("Single")
|
||||
|
||||
// Row preview (similar to the screenshot)
|
||||
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 {
|
||||
ParallelogramRow(
|
||||
count: 5,
|
||||
itemSize: CGSize(width: 18, height: 20),
|
||||
shear: -0.3,
|
||||
cornerRadius: 2,
|
||||
color: .black,
|
||||
spacing: 2
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
)
|
||||
.frame(height: 64)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.previewDisplayName("Row")
|
||||
}
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@ -1,202 +0,0 @@
|
||||
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