OpenHarmony Path ArkUI 高性能 motionPath 动效 三次贝塞尔曲线 曲线动画 SwiftUI
SwiftUI通过Path可以绘制路径动画,通过addCurve可用绘制三次贝塞尔曲线。
ArkUI是鸿蒙的核心UI布局框架,使用motionPath绘制路径动画,通过绘制路径可以自定义三次贝塞尔曲线。
对比分析
SwiftUI | ArkUI |
---|---|
Path | motionPath |
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
)
在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 })
硬编码容易出现拼写错误,参数不易理解问题。
SwiftUI通过Path应用实践
通过以下三个结构体可以完成一个简单的路径动画:
- 贝塞尔曲线动画
- 路径动画
- 构建视图
实践代码
贝塞尔曲线动画
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
}
}
路径动画
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)
}
}
最后,构建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)
}
}