MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SwiftUI Shape与Path绘制

2021-06-283.2k 阅读

SwiftUI 中的 Shape 协议

在 SwiftUI 里,Shape 是一个非常重要的协议,它定义了绘制自定义图形的基础方法。任何遵循 Shape 协议的类型,都可以在 SwiftUI 的视图层次结构中作为一个视图来展示,并且能够利用 SwiftUI 强大的布局和动画系统。

Shape 协议要求实现一个 path(in rect: CGRect) 方法,这个方法返回一个 Path 实例,描述了该形状在给定矩形范围内的几何轮廓。

下面来看一个简单的自定义形状示例,我们创建一个圆角三角形:

struct RoundedTriangle: Shape {
    func path(in rect: CGRect) -> Path {
        let path = Path { p in
            let x = rect.midX
            let y = rect.minY
            let width = rect.width
            let height = rect.height
            p.move(to: CGPoint(x: x, y: y))
            p.addLine(to: CGPoint(x: x + width / 2, y: y + height))
            p.addLine(to: CGPoint(x: x - width / 2, y: y + height))
            p.addLine(to: CGPoint(x: x, y: y))
            p.closeSubpath()
            // 这里可以添加圆角相关操作,比如使用 addArc 方法等
        }
        return path
    }
}

然后在视图中使用这个形状:

struct ContentView: View {
    var body: some View {
        RoundedTriangle()
          .fill(Color.blue)
          .frame(width: 200, height: 200)
    }
}

Path 的基础操作

Path 类是 SwiftUI 中用于描述复杂几何形状的核心类。它提供了一系列方法来构建路径,例如 move(to:)addLine(to:)addArc(center:radius:startAngle:endAngle:clockwise:) 等。

move(to:) 方法

move(to:) 方法用于设置路径的起始点。它将当前绘制位置移动到指定的 CGPoint,而不会绘制任何线条。

let path = Path { p in
    p.move(to: CGPoint(x: 100, y: 100))
    // 后续可以从这个点开始绘制线条
}

addLine(to:) 方法

addLine(to:) 方法用于从当前位置到指定的 CGPoint 绘制一条直线。它会自动将当前位置更新为新的终点。

let path = Path { p in
    p.move(to: CGPoint(x: 100, y: 100))
    p.addLine(to: CGPoint(x: 200, y: 200))
}

addArc(center:radius:startAngle:endAngle:clockwise:) 方法

addArc(center:radius:startAngle:endAngle:clockwise:) 方法用于绘制一段弧线。center 是弧线的圆心,radius 是半径,startAngleendAngle 定义了弧线的起始和结束角度,clockwise 是一个布尔值,决定弧线是顺时针还是逆时针绘制。

let path = Path { p in
    let center = CGPoint(x: 150, y: 150)
    let radius: CGFloat = 50
    let startAngle = Angle(degrees: 0)
    let endAngle = Angle(degrees: 180)
    p.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
}

复杂形状的构建

通过组合 Path 的各种操作,可以创建出非常复杂的形状。例如,我们来构建一个带有缺口的圆形:

struct NotchedCircle: Shape {
    func path(in rect: CGRect) -> Path {
        let path = Path { p in
            let center = CGPoint(x: rect.midX, y: rect.midY)
            let radius = min(rect.width, rect.height) / 2 - 10
            let startAngle = Angle(degrees: 0)
            let endAngle = Angle(degrees: 360)
            p.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
            
            // 添加缺口
            let notchWidth: CGFloat = 30
            let notchHeight: CGFloat = 20
            let notchStartX = center.x - notchWidth / 2
            let notchStartY = center.y - radius
            p.move(to: CGPoint(x: notchStartX, y: notchStartY))
            p.addLine(to: CGPoint(x: notchStartX + notchWidth, y: notchStartY))
            p.addLine(to: CGPoint(x: notchStartX + notchWidth / 2, y: notchStartY + notchHeight))
        }
        return path
    }
}

在视图中使用这个形状:

struct ContentView: View {
    var body: some View {
        NotchedCircle()
          .fill(Color.green)
          .frame(width: 200, height: 200)
    }
}

Shape 的填充与描边

SwiftUI 为 Shape 提供了丰富的填充和描边选项。

填充

使用 fill(_:style:) 方法可以对形状进行填充。fill 方法接受一个 Color 参数来指定填充颜色,还可以接受一个 FillStyle 参数来指定填充样式,例如 FillStyle(eoFill: true) 可以实现奇偶填充规则。

struct ContentView: View {
    var body: some View {
        RoundedTriangle()
          .fill(Color.blue)
          .frame(width: 200, height: 200)
    }
}

描边

使用 stroke(_:style:) 方法可以对形状进行描边。stroke 方法接受一个 Color 参数来指定描边颜色,style 参数可以用来设置线宽、线帽、线连接等样式。

struct ContentView: View {
    var body: some View {
        RoundedTriangle()
          .stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap:.round, lineJoin:.round))
          .frame(width: 200, height: 200)
    }
}

动画与 Shape

SwiftUI 的动画系统可以很方便地应用到 Shape 上。例如,我们可以通过改变形状的路径来实现动画效果。

假设有一个可以动态改变大小的圆形:

struct ResizingCircle: Shape {
    var radius: CGFloat
    
    func path(in rect: CGRect) -> Path {
        let path = Path { p in
            let center = CGPoint(x: rect.midX, y: rect.midY)
            let startAngle = Angle(degrees: 0)
            let endAngle = Angle(degrees: 360)
            p.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        }
        return path
    }
}

struct ContentView: View {
    @State private var currentRadius: CGFloat = 50
    
    var body: some View {
        ResizingCircle(radius: currentRadius)
          .fill(Color.orange)
          .frame(width: 200, height: 200)
          .onTapGesture {
                withAnimation {
                    if currentRadius == 50 {
                        currentRadius = 80
                    } else {
                        currentRadius = 50
                    }
                }
            }
    }
}

在这个例子中,当用户点击圆形时,通过 withAnimation 块,currentRadius 的值会发生改变,从而动态更新圆形的半径,实现动画效果。

与其他视图的组合

Shape 可以与其他 SwiftUI 视图进行组合,创造出更丰富的界面效果。比如,我们可以在一个形状上叠加文本:

struct RoundedRectangleWithText: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 20)
              .fill(Color.gray)
              .frame(width: 200, height: 100)
            Text("Hello, SwiftUI!")
              .foregroundColor(.white)
        }
    }
}

Path 的高级应用:贝塞尔曲线

除了基本的直线和弧线操作,Path 还支持绘制贝塞尔曲线。贝塞尔曲线是一种在计算机图形学中广泛使用的曲线,它通过控制点来定义曲线的形状。

二次贝塞尔曲线

addQuadCurve(to:control:) 方法用于绘制二次贝塞尔曲线。to 是曲线的终点,control 是控制点。

let path = Path { p in
    p.move(to: CGPoint(x: 100, y: 100))
    p.addQuadCurve(to: CGPoint(x: 200, y: 200), control: CGPoint(x: 150, y: 50))
}

三次贝塞尔曲线

addCurve(to:control1:control2:) 方法用于绘制三次贝塞尔曲线。to 是曲线的终点,control1control2 是两个控制点。

let path = Path { p in
    p.move(to: CGPoint(x: 100, y: 100))
    p.addCurve(to: CGPoint(x: 200, y: 200), control1: CGPoint(x: 120, y: 50), control2: CGPoint(x: 180, y: 150))
}

通过合理运用贝塞尔曲线,可以创建出各种流畅、自然的形状,比如水滴形状、云朵形状等。

基于 Path 的交互式绘图

在 SwiftUI 中,可以结合手势识别来实现基于 Path 的交互式绘图。例如,我们可以实现一个简单的手绘涂鸦功能。

struct DrawingView: View {
    @State private var path = Path()
    @State private var currentPoint: CGPoint?
    
    var body: some View {
        ZStack {
            Path { p in
                p.addPath(path)
            }
          .stroke(Color.black, lineWidth: 5)
            
            Rectangle()
              .fill(Color.clear)
              .gesture(
                    DragGesture()
                      .onChanged { value in
                            if let current = currentPoint {
                                path.addLine(to: value.location)
                                currentPoint = value.location
                            } else {
                                currentPoint = value.location
                            }
                        }
                      .onEnded { _ in
                            currentPoint = nil
                        }
                )
        }
    }
}

在这个 DrawingView 中,我们使用 DragGesture 来捕获用户的拖动操作。当用户开始拖动时,记录起始点;在拖动过程中,不断向 Path 中添加线条;当用户结束拖动时,重置当前点。这样就实现了一个简单的手绘涂鸦功能。

Shape 的性能优化

在使用 ShapePath 进行复杂图形绘制时,性能优化是一个重要的考虑因素。

减少路径复杂度

尽量简化路径的构建,避免过多的控制点和复杂的曲线。复杂的路径会增加渲染的计算量。

缓存路径

如果形状的路径不会频繁变化,可以考虑缓存路径。例如,在一个静态的自定义形状中,可以在初始化时计算好路径并存储起来,避免每次 path(in:) 方法调用时都重新计算。

struct StaticShape: Shape {
    private let cachedPath: Path
    
    init() {
        let path = Path { p in
            // 构建路径的代码
        }
        cachedPath = path
    }
    
    func path(in rect: CGRect) -> Path {
        return cachedPath
    }
}

合理使用动画

动画虽然能增强用户体验,但过度的动画或者对复杂形状的频繁动画更新也会影响性能。尽量减少不必要的动画,或者对动画进行合理的优化,比如设置合适的动画时长、帧率等。

总结 Shape 与 Path 的绘制要点

通过深入理解 Shape 协议和 Path 的各种操作,我们可以在 SwiftUI 中创建出各种自定义的图形,从简单的几何形状到复杂的手绘图案,并且能够将它们与 SwiftUI 的其他特性,如动画、布局和交互等相结合,打造出丰富多样的用户界面。在实际应用中,要注意性能优化,确保用户体验的流畅性。无论是开发游戏、绘图应用还是其他类型的 iOS 应用,掌握 ShapePath 的绘制技巧都能为应用增色不少。同时,不断探索和尝试新的形状组合和动画效果,能够让我们充分发挥 SwiftUI 的强大功能,创造出独具特色的应用界面。