SwiftUI中Shape内多路径绘制与动画实现问题
问题:实现+与✓的可逆切换动画
想要实现如下动画效果:
具体需求
- 初始根据
completed布尔值显示对应形状(+或✓) - 从+切换到✓:
- 水平描边透明度变为0
- 竖线顶端从(midX, minY)逆时针移动到(minX, midY),形成✓的左描边(底端固定)
- ✓的右描边从(midX, maxY)动画延伸到(maxX, minY)
- 从✓切换到+:
- ✓的右描边从(maxX, minY)反向回到(midX, maxY)
- 左描边动画回到(midX, minY),恢复为+的竖线
- 最后水平描边透明度恢复为1
- 动画需可逆;快速点击时忽略当前动画,直接切换最终状态,松开后根据最终
completed完成动画
当前问题
已使用不同线段Shape实现,但存在两个问题:
- ✓的右描边未实现正确的路径绘制动画
- 形状切换时动画反转逻辑不正确
用户原始代码
自定义Shape部分
struct LineSegment1: Shape { func path(in rect: CGRect) -> Path { var path = Path() // MARK: + shape's horizontal line path.move(to: CGPoint(x: rect.minX, y: rect.midY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) return path } } struct LineSegment2: Shape { var endPoint: CGPoint var animatableData: AnimatablePair<CGFloat, CGFloat> { get { AnimatablePair(endPoint.x, endPoint.y) } set { endPoint.x = newValue.first endPoint.y = newValue.second } } // MARK: + shape's vertical line & ☑️ shape's left stroke func path(in rect: CGRect) -> Path { let start = CGPoint(x: rect.midX, y: rect.maxY) let end = CGPoint(x: endPoint.x * rect.width, y: endPoint.y * rect.height) var path = Path() path.move(to: start) path.addLine(to: end) return path } } struct LineSegment3: Shape { var animatableData: CGFloat { get { return progress } set { progress = newValue } } var progress: CGFloat = 0 func path(in rect: CGRect) -> Path { var path = Path() // MARK: ☑️ shape's, right stroke path.move(to: CGPoint(x: rect.midX, y: rect.maxY)) path.addLine(to: CGPoint(x: rect.minX + (rect.maxX * progress), y: rect.minY)) return path } }
主视图部分
struct ContentView: View { @State private var drawProgress: CGFloat = 0.0 @State private var completed: Bool = false var body: some View { VStack { RoundedRectangle(cornerRadius: 5) .frame(width: 300, height: 300) .foregroundColor(.red) .overlay { ZStack { LineSegment1() .stroke(.white, lineWidth: 4.0) .opacity(completed ? 0 : 1) .frame(width: 200, height: 200) LineSegment2(endPoint: CGPoint( x: completed ? 0.0 : 0.5, y: completed ? 0.5 : 0.0)) .stroke(completed ? .yellow : .white, lineWidth: 4.0) .frame(width: 200, height: 200) LineSegment3(progress: drawProgress) .stroke(.yellow, lineWidth: 4.0) .opacity(completed ? 1 : 0) .frame(width: 200, height: 200) } } Button(completed ? "Incomplete" : "Complete") { withAnimation(.easeInOut(duration: 0.5)) { drawProgress = 1.0 completed.toggle() } } .buttonStyle(.borderedProminent) Spacer() } .padding() } }
解决方案
修复要点
- 修正右描边的路径计算:原
LineSegment3的终点计算逻辑错误,改为基于矩形相对位置的线性插值,确保从midX,maxY到maxX,minY的平滑过渡。 - 同步动画状态与
completed逻辑:切换时根据completed的目标状态设置drawProgress,使用Transaction处理快速点击的动画中断,保证状态正确同步。 - 调整初始状态匹配:添加
onAppear初始化drawProgress,确保初始显示与completed状态一致。
修复后的代码
自定义Shape部分
struct LineSegment1: Shape { func path(in rect: CGRect) -> Path { var path = Path() // +的水平线段 path.move(to: CGPoint(x: rect.minX, y: rect.midY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) return path } } struct LineSegment2: Shape { var endPoint: CGPoint var animatableData: AnimatablePair<CGFloat, CGFloat> { get { AnimatablePair(endPoint.x, endPoint.y) } set { endPoint.x = newValue.first endPoint.y = newValue.second } } // +的竖直线段 & ✓的左描边 func path(in rect: CGRect) -> Path { let start = CGPoint(x: rect.midX, y: rect.maxY) // 转换为绝对坐标 let end = CGPoint( x: rect.minX + endPoint.x * rect.width, y: rect.minY + endPoint.y * rect.height ) var path = Path() path.move(to: start) path.addLine(to: end) return path } } struct LineSegment3: Shape { var progress: CGFloat = 0 var animatableData: CGFloat { get { progress } set { progress = newValue } } func path(in rect: CGRect) -> Path { var path = Path() let start = CGPoint(x: rect.midX, y: rect.maxY) // 线性插值计算终点:从midX,maxY到maxX,minY let endX = rect.midX + (rect.maxX - rect.midX) * progress let endY = rect.maxY - (rect.maxY - rect.minY) * progress let end = CGPoint(x: endX, y: endY) path.move(to: start) path.addLine(to: end) return path } }
主视图部分
struct ContentView: View { @State private var drawProgress: CGFloat = 0.0 @State private var completed: Bool = false var body: some View { VStack { RoundedRectangle(cornerRadius: 5) .frame(width: 300, height: 300) .foregroundColor(.red) .overlay { ZStack { // 水平线段:未完成时显示 LineSegment1() .stroke(.white, lineWidth: 4.0) .opacity(completed ? 0 : 1) .frame(width: 200, height: 200) // 竖直线段/左描边:根据completed切换终点 LineSegment2(endPoint: CGPoint( x: completed ? 0 : 0.5, y: completed ? 0.5 : 0 )) .stroke(completed ? .yellow : .white, lineWidth: 4.0) .frame(width: 200, height: 200) // 右描边:完成时显示,按进度绘制 LineSegment3(progress: drawProgress) .stroke(.yellow, lineWidth: 4.0) .opacity(completed ? 1 : 0) .frame(width: 200, height: 200) } } Button(completed ? "标记未完成" : "标记完成") { // 用Transaction处理动画,支持快速点击中断 withTransaction(Transaction(animation: .easeInOut(duration: 0.5))) { completed.toggle() // 根据目标状态设置进度 drawProgress = completed ? 1 : 0 } } .buttonStyle(.borderedProminent) Spacer() } .padding() // 初始状态同步 .onAppear { drawProgress = completed ? 1 : 0 } } }
效果说明
- 右描边现在会从
midX,maxY平滑延伸到maxX,minY,反向切换时也会原路退回 - 快速点击按钮时,当前动画会立即中断,直接切换到最终状态并完成对应动画
- 初始显示与
completed状态完全匹配,无错位问题
内容的提问来源于stack exchange,提问作者batman
相关产品推荐
相关产品推荐

