You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 09:37:10