You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 18:24:39