SwiftUI:如何用Path绘制圆角三角形并复刻灵动岛指定视图
灵动岛实时活动视图实现问题解决
需求说明
我正在复刻苹果文档中的灵动岛(Dynamic Island)和实时活动(Live Activities)视图,已完成圆角弧形绘制,当前需要解决两个问题:
- 给用Path实现的披萨状图形添加圆角(尝试
.cornerRadius和.clipShape无效) - 添加旋转的小型卫星视图
当前代码
Arc 结构体
struct Arc: Shape { /// 绘制比例:1代表100%,0.5代表一半 let percent: CGFloat /// 线条宽度:用于偏移圆弧在CGRect中的位置 let lineWidth: CGFloat init(percent: CGFloat = 1, lineWidth: CGFloat = 1) { self.percent = percent self.lineWidth = lineWidth } func path(in rect: CGRect) -> Path { let rect = rect.insetBy(dx: lineWidth / 2, dy: lineWidth / 2) var path = Path() let x = rect.width / 2 let y = rect.height let w = sqrt(x * x + y * y) let phi = atan2(x, y) let radius = w / 2 / cos(phi) let center = CGPoint(x: rect.minX + x, y: rect.minY + radius) let theta = 2 * (.pi / 2 - phi) * percent let startAngle = Angle(radians: 3 * .pi / 2 - theta) let endAngle = Angle(radians: 3 * .pi / 2 + theta) path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) return path } }
原披萨状图形结构体(无圆角)
struct RoundedTriangleShape : Shape { func path(in rect: CGRect) -> Path { var path = Path() let x = rect.width / 2 let y = rect.height let w = sqrt(x * x + y * y) let phi = atan2(x, y) let radius = w / 2 / cos(phi) let center = CGPoint(x: rect.minX + x, y: rect.minY + radius) let startAngle: Angle = .degrees(250) let endAngle: Angle = .degrees(290) path.move(to: CGPoint(x: rect.midX, y: rect.midY + y / 1.5)) path.addArc(center: center, radius: radius - 10, startAngle: startAngle, endAngle: endAngle, clockwise: false) path.closeSubpath() return path } }
灵动岛展开区域代码
DynamicIslandExpandedRegion(.bottom) { GeometryReader { geometry in HStack { Spacer() ZStack { Arc(lineWidth: 20) .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round)) .foregroundStyle(.indigo.opacity(0.5)) RoundedTriangleShape() .foregroundStyle(.linearGradient(colors: [.blue, .cyan], startPoint: .bottom, endPoint: .top).opacity(0.5)) Arc(percent: 0.35, lineWidth: 20) .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round)) .foregroundStyle(.cyan) } .frame(width: geometry.size.width / 1.2, height: geometry.size.width / 5, alignment: .center) Spacer() } } .frame(height: 300) }
解决方案
1. 给披萨状图形添加圆角
直接使用.cornerRadius无效是因为自定义Shape的尖角是通过closeSubpath直接连接产生的,需要手动在三个角绘制圆弧来实现圆角。修改后的结构体如下:
struct RoundedPizzaShape: Shape { let cornerRadius: CGFloat // 自定义圆角半径 func path(in rect: CGRect) -> Path { var path = Path() let x = rect.width / 2 let y = rect.height let w = sqrt(x * x + y * y) let phi = atan2(x, y) let radius = w / 2 / cos(phi) let center = CGPoint(x: rect.minX + x, y: rect.minY + radius) let arcRadius = radius - 10 // 计算圆弧的起始点、结束点和底部顶点 let startAngleRad = Angle.degrees(250).radians let endAngleRad = Angle.degrees(290).radians let startPoint = CGPoint( x: center.x + arcRadius * cos(startAngleRad), y: center.y + arcRadius * sin(startAngleRad) ) let endPoint = CGPoint( x: center.x + arcRadius * cos(endAngleRad), y: center.y + arcRadius * sin(endAngleRad) ) let bottomPoint = CGPoint(x: rect.midX, y: rect.midY + y / 1.5) // 绘制底部左侧圆角 path.move(to: CGPoint( x: bottomPoint.x + cornerRadius * cos(.pi * 0.7), y: bottomPoint.y + cornerRadius * sin(.pi * 0.7) )) path.addArc( center: CGPoint( x: bottomPoint.x, y: bottomPoint.y - cornerRadius ), radius: cornerRadius, startAngle: .degrees(225), endAngle: .degrees(135), clockwise: false ) // 绘制主体圆弧 path.addArc( center: center, radius: arcRadius, startAngle: .degrees(250), endAngle: .degrees(290), clockwise: false ) // 绘制底部右侧圆角 path.addArc( center: CGPoint( x: bottomPoint.x, y: bottomPoint.y - cornerRadius ), radius: cornerRadius, startAngle: .degrees(45), endAngle: .degrees(-45), clockwise: true ) path.closeSubpath() return path } }
使用时替换原RoundedTriangleShape,传入圆角半径即可:
RoundedPizzaShape(cornerRadius: 8) .foregroundStyle(.linearGradient(colors: [.blue, .cyan], startPoint: .bottom, endPoint: .top).opacity(0.5))
2. 添加旋转的小型卫星视图
通过RotationEffect结合无限重复的动画实现旋转,将卫星视图叠加在主视图ZStack中:
第一步:定义卫星视图
struct SatelliteView: View { var body: some View { Circle() .frame(width: 16, height: 16) .foregroundStyle(.orange) } }
第二步:在灵动岛区域添加旋转动画
DynamicIslandExpandedRegion(.bottom) { GeometryReader { geometry in HStack { Spacer() ZStack { Arc(lineWidth: 20) .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round)) .foregroundStyle(.indigo.opacity(0.5)) RoundedPizzaShape(cornerRadius: 8) .foregroundStyle(.linearGradient(colors: [.blue, .cyan], startPoint: .bottom, endPoint: .top).opacity(0.5)) Arc(percent: 0.35, lineWidth: 20) .stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round)) .foregroundStyle(.cyan) // 旋转卫星视图 SatelliteView() .offset(x: (geometry.size.width / 1.2) / 2) // 偏移到圆弧外侧 .rotationEffect(.degrees(0)) .animation(Animation.linear(duration: 4).repeatForever(autoreverses: false), value: UUID()) .rotationEffect(.degrees(360)) // 如需多个卫星,复制并调整初始角度 SatelliteView() .offset(x: (geometry.size.width / 1.2) / 2) .rotationEffect(.degrees(120)) .animation(Animation.linear(duration: 4).repeatForever(autoreverses: false), value: UUID()) .rotationEffect(.degrees(360)) } .frame(width: geometry.size.width / 1.2, height: geometry.size.width / 5, alignment: .center) Spacer() } } .frame(height: 300) }
解释:通过offset将卫星定位到圆弧外侧,利用rotationEffect和无限循环的线性动画实现持续旋转,多个卫星可通过设置不同初始角度错开位置。
内容的提问来源于stack exchange,提问作者SantiOch
相关产品推荐
相关产品推荐

