You need to enable JavaScript to run this app. Core Graphics结合DisplayLink实现弹跳球时出现异常行为求助
优惠活动
大模型
产品
解决方案
定价
更多

Core Graphics结合DisplayLink实现弹跳球时出现异常行为求助

问题根因

  • UIView的clearsContextBeforeDrawing属性默认值为true,每次系统调用draw(_ rect:)前,会自动将绘制区域填充为透明,清除上一帧的所有内容,你没有主动清除不代表系统不会执行清空操作。
  • 你将小球的碰撞检测、位置更新等状态变更逻辑写在了draw(_ rect:)方法中,这不符合UIKit的设计规范:draw(_ rect:)的唯一作用是绘制当前视图的状态,不应该包含任何状态修改逻辑。系统可能在多种场景下主动触发draw(_ rect:)调用(比如视图大小变更、从遮挡状态重新露出),每次调用都会额外执行一次位置更新,直接导致小球运动速度、轨迹不符合预期,也是画面闪烁、内容偏移的核心原因。
  • 你想要保留历史绘制痕迹的需求,不能直接依赖系统提供的实时绘制上下文实现,因为该上下文每次重绘都会被重置,你需要自行缓存历史绘制内容。

修复方案

基础修复(无轨迹、正常弹跳无闪烁)

  1. 将小球物理更新逻辑从draw(_ rect:)中剥离,单独封装更新方法,在DisplayLink的回调中先执行状态更新,再触发重绘
  2. 给自定义视图设置背景色,每次重绘时系统会自动填充背景覆盖上一帧内容,避免闪烁

修改后的核心代码如下:

class ViewController: UIViewController {
    let myView = MyView()
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBlue
        myView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myView)
        NSLayoutConstraint.activate([
            myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            myView.widthAnchor.constraint(equalTo: view.widthAnchor),
            myView.heightAnchor.constraint(equalTo: view.heightAnchor)
        ])
        createDisplayLink(fps: 60)
    }
    func createDisplayLink(fps: Int) {
        let displaylink = CADisplayLink(target: self, selector: #selector(step))
        displaylink.preferredFramesPerSecond = fps
        displaylink.add(to: .current, forMode: .default)
    }
    @objc func step(displaylink: CADisplayLink) {
        // 先更新状态,再触发重绘
        myView.updateBallsState()
        myView.setNeedsDisplay()
    }
}

class MyView: UIView {
    let numBalls = 5
    var balls = [Ball]()
    override init(frame:CGRect) {
        super.init(frame:frame)
        // 设置背景色,避免闪烁
        backgroundColor = .systemBlue
        for _ in 0..<numBalls {
            balls.append(
                Ball(
                    ballPosition: Vec2(x: CGFloat.random(in: 0...UIScreen.main.bounds.width), y: CGFloat.random(in: 0...UIScreen.main.bounds.height)),
                    ballSpeed: Vec2(x: CGFloat.random(in: 0.5...2), y: CGFloat.random(in: 0.5...2))))
        }
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    // 单独封装状态更新方法
    func updateBallsState() {
        for i in 0..<numBalls {
            if balls[i].ballPosition.x > bounds.width - balls[i].ballSize || balls[i].ballPosition.x < 0 {
                balls[i].ballSpeed.x *= -1
            }
            balls[i].ballPosition.x += balls[i].ballSpeed.x
            if balls[i].ballPosition.y > bounds.height - balls[i].ballSize || balls[i].ballPosition.y < 0 {
                balls[i].ballSpeed.y *= -1
            }
            balls[i].ballPosition.y += balls[i].ballSpeed.y
        }
    }
    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        // 只做绘制逻辑,不修改状态
        for i in 0..<numBalls {
            context.setFillColor(UIColor.red.cgColor)
            context.setStrokeColor(UIColor.green.cgColor)
            context.setLineWidth(0)
            let rectangle = CGRect(x: balls[i].ballPosition.x, y: balls[i].ballPosition.y, width: balls[i].ballSize, height: balls[i].ballSize)
            context.addEllipse(in: rectangle)
            context.drawPath(using: .fillStroke)
        }
    }
}

保留运动轨迹的实现

如果需要保留小球的运动痕迹,需要自行创建离屏上下文缓存历史绘制内容,修改MyView的绘制逻辑如下:

class MyView: UIView {
    let numBalls = 5
    var balls = [Ball]()
    // 缓存历史绘制内容的离屏上下文
    private var cacheContext: CGContext?
    override init(frame:CGRect) {
        super.init(frame:frame)
        backgroundColor = .systemBlue
        for _ in 0..<numBalls {
            balls.append(
                Ball(
                    ballPosition: Vec2(x: CGFloat.random(in: 0...UIScreen.main.bounds.width), y: CGFloat.random(in: 0...UIScreen.main.bounds.height)),
                    ballSpeed: Vec2(x: CGFloat.random(in: 0.5...2), y: CGFloat.random(in: 0.5...2))))
        }
        // 初始化离屏缓存上下文
        let scale = UIScreen.main.scale
        UIGraphicsBeginImageContextWithOptions(frame.size, false, scale)
        cacheContext = UIGraphicsGetCurrentContext()
        // 初始填充背景色
        cacheContext?.setFillColor(UIColor.systemBlue.cgColor)
        cacheContext?.fill(frame)
        UIGraphicsEndImageContext()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func updateBallsState() {
        // 状态更新逻辑和上面一致
        for i in 0..<numBalls {
            if balls[i].ballPosition.x > bounds.width - balls[i].ballSize || balls[i].ballPosition.x < 0 {
                balls[i].ballSpeed.x *= -1
            }
            balls[i].ballPosition.x += balls[i].ballSpeed.x
            if balls[i].ballPosition.y > bounds.height - balls[i].ballSize || balls[i].ballPosition.y < 0 {
                balls[i].ballSpeed.y *= -1
            }
            balls[i].ballPosition.y += balls[i].ballSpeed.y
        }
    }
    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext(), let cacheContext = cacheContext else { return }
        // 先把当前小球绘制到缓存上下文
        for i in 0..<numBalls {
            cacheContext.setFillColor(UIColor.red.cgColor)
            cacheContext.setStrokeColor(UIColor.green.cgColor)
            cacheContext.setLineWidth(0)
            let rectangle = CGRect(x: balls[i].ballPosition.x, y: balls[i].ballPosition.y, width: balls[i].ballSize, height: balls[i].ballSize)
            cacheContext.addEllipse(in: rectangle)
            cacheContext.drawPath(using: .fillStroke)
        }
        // 把缓存的所有历史内容绘制到当前上下文中
        if let cacheImage = cacheContext.makeImage() {
            context.draw(cacheImage, in: bounds)
        }
    }
}

内容的提问来源于stack exchange,提问作者rikitikitavi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.13 21:51:01