110 lines
4.9 KiB
Swift
110 lines
4.9 KiB
Swift
import SwiftUI
|
||
|
||
/// 顶部带“内凹”弧形的连续圆角矩形
|
||
struct ScoopRoundedRect: Shape {
|
||
var cornerRadius: CGFloat = 20
|
||
/// 凹凸的“深度”:>0 向下凸(更接近你截图的效果),<0 为向上凹
|
||
var scoopDepth: CGFloat = 10
|
||
/// 半宽:控制水平占据范围(越大越“平缓”)
|
||
var scoopHalfWidth: CGFloat = 18
|
||
/// 相对位置(0~1,0.5 正中)
|
||
var scoopCenterX: CGFloat = 0.33
|
||
/// 是否向下凸;为 false 时表现为向上“凹”(与 notch 类似)
|
||
var convexDown: Bool = true
|
||
/// 凹陷/鼓包底部的“平底”半宽(0 为无平底)
|
||
var flatHalfWidth: CGFloat = 8
|
||
|
||
func path(in rect: CGRect) -> Path {
|
||
let r = min(cornerRadius, min(rect.width, rect.height) * 0.5)
|
||
let topY = rect.minY
|
||
|
||
// 约束中心与范围,避免穿出圆角
|
||
let minX = rect.minX + r
|
||
let maxX = rect.maxX - r
|
||
let centerX = rect.minX + rect.width * scoopCenterX
|
||
let hw = min(scoopHalfWidth, (maxX - minX) * 0.45)
|
||
let flatHW = max(0, min(flatHalfWidth, hw * 0.8))
|
||
let shoulder = max(1, hw - flatHW) // 两侧曲线的水平长度
|
||
let startX = max(minX, centerX - (flatHW + shoulder))
|
||
let endX = min(maxX, centerX + (flatHW + shoulder))
|
||
let leftFlatX = max(minX, centerX - flatHW)
|
||
let rightFlatX = min(maxX, centerX + flatHW)
|
||
let depth = (convexDown ? 1 : -1) * scoopDepth
|
||
|
||
// 左曲线:P0 -> Lf(水平);右曲线:Rf -> P3(水平);Lf~Rf 之间是一段水平直线
|
||
let P0 = CGPoint(x: startX, y: topY)
|
||
let Lf = CGPoint(x: leftFlatX, y: topY + depth)
|
||
let Rf = CGPoint(x: rightFlatX, y: topY + depth)
|
||
let P3 = CGPoint(x: endX, y: topY)
|
||
|
||
// 使用圆角近似系数控制手柄长度(基于 shoulder)
|
||
let k = shoulder * 0.5522847498
|
||
let C1 = CGPoint(x: P0.x + k, y: P0.y) // P0 水平切线
|
||
let C2 = CGPoint(x: Lf.x - k, y: Lf.y) // Lf 水平切线
|
||
let C3 = CGPoint(x: Rf.x + k, y: Rf.y) // Rf 水平切线
|
||
let C4 = CGPoint(x: P3.x - k, y: P3.y) // P3 水平切线
|
||
|
||
var p = Path()
|
||
// 顶部从左上圆角起
|
||
p.move(to: CGPoint(x: rect.minX + r, y: topY))
|
||
// 左侧直线到凹/凸开始
|
||
p.addLine(to: P0)
|
||
// 左侧进入曲线
|
||
p.addCurve(to: Lf, control1: C1, control2: C2)
|
||
// 平底水平直线
|
||
p.addLine(to: Rf)
|
||
// 右侧离开曲线
|
||
p.addCurve(to: P3, control1: C3, control2: C4)
|
||
// 顶部到右上圆角
|
||
p.addLine(to: CGPoint(x: rect.maxX - r, y: topY))
|
||
// 右上圆角
|
||
p.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.minY + r),
|
||
control: CGPoint(x: rect.maxX, y: rect.minY))
|
||
// 右侧直线到右下圆角
|
||
p.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - r))
|
||
// 右下圆角
|
||
p.addQuadCurve(to: CGPoint(x: rect.maxX - r, y: rect.maxY),
|
||
control: CGPoint(x: rect.maxX, y: rect.maxY))
|
||
// 底边到左下圆角
|
||
p.addLine(to: CGPoint(x: rect.minX + r, y: rect.maxY))
|
||
// 左下圆角
|
||
p.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - r),
|
||
control: CGPoint(x: rect.minX, y: rect.maxY))
|
||
// 左侧到左上圆角
|
||
p.addLine(to: CGPoint(x: rect.minX, y: rect.minY + r))
|
||
// 左上圆角
|
||
p.addQuadCurve(to: CGPoint(x: rect.minX + r, y: rect.minY),
|
||
control: CGPoint(x: rect.minX, y: rect.minY))
|
||
p.closeSubpath()
|
||
return p
|
||
}
|
||
}
|
||
|
||
struct ScoopRoundedRect_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
VStack(spacing: 30) {
|
||
// 向下“鼓包”且底部有平直段
|
||
ScoopRoundedRect(cornerRadius: 24, scoopDepth: 8, scoopHalfWidth: 26, scoopCenterX: 0.25, convexDown: true, flatHalfWidth: 12)
|
||
.fill(Color.orange)
|
||
.frame(height: 140)
|
||
.shadow(color: .black.opacity(0.08), radius: 12, y: 6)
|
||
.padding()
|
||
|
||
// 中央更深、更宽,平底更宽
|
||
ScoopRoundedRect(cornerRadius: 28, scoopDepth: 12, scoopHalfWidth: 36, scoopCenterX: 0.5, convexDown: true, flatHalfWidth: 18)
|
||
.fill(Color.orange)
|
||
.frame(height: 140)
|
||
.shadow(color: .black.opacity(0.08), radius: 12, y: 6)
|
||
.padding()
|
||
|
||
// 作为对比:向上“凹陷”的 notch,带平底
|
||
ScoopRoundedRect(cornerRadius: 24, scoopDepth: 10, scoopHalfWidth: 22, scoopCenterX: 0.6, convexDown: false, flatHalfWidth: 10)
|
||
.fill(Color.orange)
|
||
.frame(height: 140)
|
||
.shadow(color: .black.opacity(0.08), radius: 12, y: 6)
|
||
.padding()
|
||
}
|
||
.background(Color(white: 0.96))
|
||
.previewLayout(.sizeThatFits)
|
||
}
|
||
} |