使用矢量算法的SwiftUI线图动画

问题描述

寻找提高绘图性能方法(希望不使用Metal)。将LinearGradient添加为Shape的填充对绘制性能有很大的影响,因此我将其省略。

向量

struct LineGraphVector: VectorArithmetic {
    var points: [CGPoint.AnimatableData]
    
    static func + (lhs: LineGraphVector,rhs: LineGraphVector) -> LineGraphVector {
        return add(lhs: lhs,rhs: rhs,+)
    }
    
    static func - (lhs: LineGraphVector,-)
    }
    
    static func add(lhs: LineGraphVector,rhs: LineGraphVector,_ sign: (CGFloat,CGFloat) -> CGFloat) -> LineGraphVector {
        let maxPoints = max(lhs.points.count,rhs.points.count)
        let leftIndices = lhs.points.indices
        let rightIndices = rhs.points.indices
        
        var newPoints: [CGPoint.AnimatableData] = []
        (0 ..< maxPoints).forEach { index in
            if leftIndices.contains(index) && rightIndices.contains(index) {
                // Merge points
                let lhsPoint = lhs.points[index]
                let rhsPoint = rhs.points[index]
                newPoints.append(
                    .init(
                        sign(lhsPoint.first,rhsPoint.first),sign(lhsPoint.second,rhsPoint.second)
                    )
                )
            } else if rightIndices.contains(index),let lastLeftPoint = lhs.points.last {
                // Right side has more points,collapse to last left point
                let rightPoint = rhs.points[index]
                newPoints.append(
                    .init(
                        sign(lastLeftPoint.first,rightPoint.first),sign(lastLeftPoint.second,rightPoint.second)
                    )
                )
            } else if leftIndices.contains(index),let lastPoint = newPoints.last {
                // Left side has more points,collapse to last kNown point
                let leftPoint = lhs.points[index]
                newPoints.append(
                    .init(
                        sign(lastPoint.first,leftPoint.first),sign(lastPoint.second,leftPoint.second)
                    )
                )
            }
        }
        
        return .init(points: newPoints)
    }
    
    mutating func scale(by rhs: Double) {
        points.indices.forEach { index in
            self.points[index].scale(by: rhs)
        }
    }
    
    var magnitudeSquared: Double {
        return 1.0
    }
    
    static var zero: LineGraphVector {
        return .init(points: [])
    }
}

形状

struct LineGraphShape: Shape {
    var points: [CGPoint]
    let closePath: Bool
    
    init(points: [CGPoint],closePath: Bool) {
        self.points = points
        self.closePath = closePath
    }
    
    var animatableData: LineGraphVector {
        get { .init(points: points.map { CGPoint.AnimatableData($0.x,$0.y) }) }
        set { points = newValue.points.map { CGPoint(x: $0.first,y: $0.second) } }
    }
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: points.first ?? .zero)
            path.addLines(points)
            
            switch (closePath,points.first,points.last) {
            case (true,.some(let firstPoint),.some(let lastPoint)):
                path.addLine(to: .init(x: lastPoint.x,y: rect.height))
                path.addLine(to: .init(x: 0.0,y: firstPoint.y))
                path.closeSubpath()
            default:
                break
            }
        }
    }
}

用法

struct ContentView: View {
    static let firstPoints: [CGPoint] = [
        .init(x: 0.0,y: 0.0),.init(x: 20.0,y: 320.0),.init(x: 40.0,y: 50.0),.init(x: 60.0,y: 10.0),.init(x: 90.0,y: 140.0),.init(x: 200.0,y: 60.0),.init(x: 420.0,y: 20.0),]
    
    static let secondPoints: [CGPoint] = [
        .init(x: 0.0,.init(x: 10.0,y: 200.0),.init(x: 30.0,y: 70.0),y: 90.0),.init(x: 50.0,y: 150.0),y: 120.0),.init(x: 70.0,.init(x: 80.0,y: 30.0),.init(x: 100.0,.init(x: 110.0,.init(x: 120.0,.init(x: 130.0,.init(x: 140.0,.init(x: 150.0,.init(x: 160.0,.init(x: 170.0,.init(x: 180.0,.init(x: 190.0,.init(x: 210.0,.init(x: 220.0,.init(x: 230.0,.init(x: 240.0,.init(x: 250.0,.init(x: 260.0,.init(x: 270.0,.init(x: 280.0,.init(x: 290.0,]
    
    static let thirdPoints: [CGPoint] = [
        .init(x: 0.0,y: 300.0),y: 400.0),y: 320),]
    
    let pointTypes = [0,1,2]
    @State private var selectedPointType = 0
    let points: [[CGPoint]] = [firstPoints,secondPoints,thirdPoints]
    
    var body: some View {
        vstack {
            ZStack {
                LineGraphShape(points: points[selectedPointType],closePath: true)
                    .fill(Color.blue.opacity(0.5))
                
                LineGraphShape(points: points[selectedPointType],closePath: false)
                    .stroke(
                        Color.blue,style: .init(
                            linewidth: 4.0,lineCap: .round,lineJoin: .round,miterLimit: 10.0
                        )
                    )
                
                Text("\(points[selectedPointType].count) Points")
                    .font(.largeTitle)
                    .animation(.none)
            }
            .animation(.easeInOut(duration: 2.0))
            
            vstack {
                Picker("",selection: $selectedPointType) {
                    ForEach(pointTypes,id: \.self) { pointType in
                        Text("Graph \(pointType)")
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
            }
            .padding(.horizontal,16.0)
            .padding(.bottom,32.0)
        }
    }
}

Demo

解决方法

为什么要避免使用Metal?启用它的支持就像将LineGraphShape包装到Group中并用drawingGroup()对其进行修改一样容易。试试看:

...
Group {
    let gradient = LinearGradient(
        gradient: Gradient(colors: [Color.red,Color.blue]),startPoint: .leading,endPoint: .trailing
    )
    
    LineGraphShape(points: points[selectedPointType],closePath: true)
        .fill(gradient)
    
    LineGraphShape(points: points[selectedPointType],closePath: false)
        .stroke(
            gradient,style: .init(
                lineWidth: 4.0,lineCap: .round,lineJoin: .round,miterLimit: 10.0
            )
        )
}
    .drawingGroup()
...

显着提高了性能?