iOS开发中如何不使用UIBezierPath实现UIView沿直线路径动画
适配CGContext绘制逻辑的路径动画实现方案
不需要重构你现有基于CGContext的绘制代码,所有路径动画的底层能力都是基于CGPath实现的,UIBezierPath只是上层封装,你只需要做最小改动即可实现效果,不需要把所有绘制逻辑替换成UIBezierPath。
1. 新增CanvasView存储属性
保留你原有的所有属性、枚举、初始化逻辑,仅新增以下几个动画相关属性:
class CanvasView: UIView { // 你原有定义的ShapeTypes枚举、其他业务属性保留不动 private var moveLinePath: CGPath? // 存储绘制的运动直线路径引用 private var playerRect: CGRect = .zero // 存储玩家矩形的实时位置 private var displayLink: CADisplayLink? private var animationProgress: CGFloat = 0 // 动画进度,取值范围0~1 // 你的原有初始化方法、awakeFromNib里记得给playerRect赋初始值,和你之前写死的玩家位置一致即可 }
2. 最小改动修改draw(_ rect:)方法
你原有的路径绘制逻辑完全不需要修改,仅做两处调整:
- 绘制完运动直线路径后,将当前上下文的路径引用赋值给
moveLinePath - 将原来写死位置的玩家矩形绘制,替换为使用
playerRect变量绘制
示例代码:
override func draw(_ rect: CGRect) { guard let ctx = UIGraphicsGetCurrentContext() else { return } // --- 以下是你原有的运动路径绘制代码,完全不需要改 --- ctx.setStrokeColor(UIColor.lightGray.cgColor) ctx.setLineWidth(2) ctx.move(to: CGPoint(x: 40, y: 200)) ctx.addLine(to: CGPoint(x: UIScreen.main.bounds.width - 40, y: 200)) ctx.strokePath() // 仅新增这一行,存下当前路径,不影响原有绘制效果 moveLinePath = ctx.path // --- 原路径绘制代码结束 --- // --- 原玩家矩形绘制,把写死的CGRect替换为self.playerRect即可 --- ctx.setFillColor(UIColor.systemBlue.cgColor) ctx.fill(playerRect) }
注意:不要在draw方法里做任何和绘制无关的逻辑,仅做变量替换即可,原有显示效果和你修改前完全一致。
3. 实现playAnimation动画逻辑
用CADisplayLink做逐帧驱动,直接基于存储的CGPath计算当前帧玩家位置,不需要依赖UIBezierPath做全量绘制:
func playAnimation() { // 先终止上一轮正在运行的动画 displayLink?.invalidate() animationProgress = 0 guard let path = moveLinePath else { return } // 初始化帧同步定时器 displayLink = CADisplayLink(target: self, selector: #selector(stepAnimation)) displayLink?.add(to: .main, forMode: .common) } @objc private func stepAnimation() { // 可自行修改动画总时长,单位秒 let animationTotalTime: CGFloat = 2.5 // 按帧时长累加进度 animationProgress += CGFloat(displayLink?.duration ?? 0) / animationTotalTime if animationProgress >= 1 { // 动画走完,终止定时器,修正终点位置 animationProgress = 1 displayLink?.invalidate() displayLink = nil } // 计算当前进度对应的路径坐标点 let currentPosition = path.interpolatedPoint(percent: animationProgress) // 更新玩家矩形位置,保持矩形尺寸不变,中心对齐到路径点 playerRect.origin = CGPoint( x: currentPosition.x - playerRect.width / 2, y: currentPosition.y - playerRect.height / 2 ) // 标记视图需要重绘,会自动触发draw(_:)刷新玩家位置 setNeedsDisplay() }
配套CGPath扩展,实现直线路径的点插值计算,直接操作CGPath,不需要转UIBezierPath:
extension CGPath { func interpolatedPoint(percent: CGFloat) -> CGPoint { let clampPercent = max(0, min(1, percent)) var pathPoints = [CGPoint]() // 遍历CGPath的元素,取出直线的起点和终点 applyWithBlock { element in let ptr = element.pointee.points switch element.pointee.type { case .moveToPoint: pathPoints.append(ptr[0]) case .addLineToPoint: pathPoints.append(ptr[0]) default: break } } guard pathPoints.count >= 2 else { return .zero } let start = pathPoints[0] let end = pathPoints[1] // 线性插值计算当前进度对应的点 return CGPoint( x: start.x + (end.x - start.x) * clampPercent, y: start.y + (end.y - start.y) * clampPercent ) } }
避坑说明
- 不要直接对CanvasView用UIView动画修改playerRect,因为draw(_:)是即时绘制的,UIView动画不会逐帧触发重绘,会出现位置跳变
- 不需要把现有CGContext绘制逻辑全部替换为UIBezierPath实现,CGPath是CoreGraphics的底层路径结构,UIBezierPath只是对它的上层封装,二者可以随时无损互转,现有逻辑完全可以保留
- 如果后续需要支持折线路径、曲线路径,只需要扩展
interpolatedPoint方法的计算逻辑即可,不需要改动上层绘制和动画驱动代码
内容的提问来源于stack exchange,提问作者Pinal
相关产品推荐
相关产品推荐

