• SwiftUI&ArkUI-曲线动画Path和路径动画motionPath


    OpenHarmony Path ArkUI 高性能 motionPath 动效 三次贝塞尔曲线 曲线动画 SwiftUI

    SwiftUI通过Path可以绘制路径动画,通过addCurve可用绘制三次贝塞尔曲线。

    ArkUI是鸿蒙的核心UI布局框架,使用motionPath绘制路径动画,通过绘制路径可以自定义三次贝塞尔曲线。

    对比分析

    SwiftUIArkUI
    PathmotionPath
    The outline of a 2D shape.设置组件进行位移动画时的运动路径。
    struct Path.motionPath({})
    addCurve绘制路径
    addCurve(to:control1:control2:)Mstart.x start.y C -200 50, -150 200, end.x end.y

    在SwiftUI中,可用通过设置三个关键点(结束点、控制1、控制2)调整贝塞尔曲线。

    mutating func addCurve(
        to end: CGPoint,
        control1: CGPoint,
        control2: CGPoint
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在ArkUI中,贝塞尔曲线的参数是硬编码,字符串各个参数中间用空格隔开。

    Button('click me').margin(50)
            // 执行动画:从起点移动到(300,200),再到(300,500),再到终点
            .motionPath({ path: 'Mstart.x start.y L300 200 L300 500 Lend.x end.y', from: 0.0, to: 1.0, rotatable: true })
    
    • 1
    • 2
    • 3

    硬编码容易出现拼写错误,参数不易理解问题。

    SwiftUI通过Path应用实践

    通过以下三个结构体可以完成一个简单的路径动画:

    1. 贝塞尔曲线动画
    2. 路径动画
    3. 构建视图

    实践代码

    贝塞尔曲线动画

    struct InfinityShape2: Shape {
        func path(in rect: CGRect) -> Path {
            return InfinityShape2.createInfinityPath(in: rect)
        }
    
        static func createInfinityPath(in rect: CGRect) -> Path {
            let height = rect.size.height
            let width = rect.size.width
            let heightFactor = height/4
            let widthFactor = width/4
    
            var path = Path()
            
            path.move(to: CGPoint(x:widthFactor, y: heightFactor * 6))
            path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor), control1: CGPoint(x:0, y: heightFactor * 6), control2: CGPoint(x:0, y: heightFactor))
    
            path.move(to: CGPoint(x:widthFactor, y: heightFactor))
            path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3), control1: CGPoint(x:widthFactor * 2, y: heightFactor), control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3))
    
            path.move(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3))
            path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor), control1: CGPoint(x:widthFactor * 4 + 5, y: heightFactor * 3), control2: CGPoint(x:widthFactor * 4 + 5, y: heightFactor))
    
            path.move(to: CGPoint(x:widthFactor * 3, y: heightFactor))
            path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor * 6), control1: CGPoint(x:widthFactor * 1, y: heightFactor), control2: CGPoint(x:widthFactor * 2, y: heightFactor * 6))
            
            return path
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    路径动画

    struct FollowEffect2: GeometryEffect {
        var pct: CGFloat = 0
        let path: Path
        var rotate = true
    
        var animatableData: CGFloat {
            get { return pct }
            set { pct = newValue }
        }
    
        func effectValue(size: CGSize) -> ProjectionTransform {
    
            if !rotate {
                let pt = percentPoint(pct)
    
                return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y))
            } else {
                // Calculate rotation angle, by calculating an imaginary line between two points
                // in the path: the current position (1) and a point very close behind in the path (2).
                let pt1 = percentPoint(pct)
                let pt2 = percentPoint(pct - 0.01)
    
                let a = pt2.x - pt1.x
                let b = pt2.y - pt1.y
    
                let angle = a < 0 ? atan(Double(b / a)) : atan(Double(b / a)) - Double.pi
    
                let transform = CGAffineTransform(translationX: pt1.x, y: pt1.y).rotated(by: CGFloat(angle))
    
                return ProjectionTransform(transform)
            }
        }
    
        func percentPoint(_ percent: CGFloat) -> CGPoint {
    
            let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
    
            let f = pct > 0.999 ? CGFloat(1-0.001) : pct
            let t = pct > 0.999 ? CGFloat(1) : pct + 0.001
            let tp = path.trimmedPath(from: f, to: t)
    
            return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    最后,构建view

    struct ExamplePlane: View {
        @State private var flag = false
    
        var body: some View {
            GeometryReader { proxy in
                ZStack(alignment: .topLeading) {
                    
                    // Draw the Infinity Shape
                    InfinityShape2().stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .miter, miterLimit: 0, dash: [7, 7], dashPhase: 0))
                        .foregroundColor(.blue)
                        .frame(width: proxy.size.width, height: 300)
    
                    // Animate movement of Image
                    // Image(systemName: "airplane")
                    Image(systemName: "airplane").resizable().foregroundColor(Color.red)
                        .frame(width: 80, height: 80).offset(x: -40, y: -40)
                        .modifier(FollowEffect2(pct: self.flag ? 1 : 0, path: InfinityShape2.createInfinityPath(in: CGRect(x: 0, y: 0, width: proxy.size.width, height: 300)), rotate: true))
                        .onAppear {
                            withAnimation(Animation.linear(duration: 4.0).repeatForever(autoreverses: false)) {
                                self.flag.toggle()
                            }
                        }
    
                    }.frame(alignment: .topLeading)
            }
            .padding(20)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

  • 相关阅读:
    Android 12.0 修改系统签名文件类型test-keys为release-keys
    【论文阅读】自动作文评分系统:一份系统的文献综述
    Ceph 架构以及部署
    Stream常用操作以及原理探索
    OpenCore引导都支持哪些.efi文件
    JAVA计算机毕业设计预防接种服务平台Mybatis+源码+数据库+lw文档+系统+调试部署
    mybatis日志、反射、DataSource
    Git 配置处理客户端无法正常访问到 github 原网站时,npm 下载依赖包失败的问题
    学习discovery studio对对接结果进行分析
    web前端期末大作业——基于html+css+javascript+jquery+bootstrap响应式户外旅游网站
  • 原文地址:https://blog.csdn.net/gwh111/article/details/133633207