Core Graphics结合DisplayLink实现弹跳球时出现异常行为求助
问题根因
- UIView的
clearsContextBeforeDrawing属性默认值为true,每次系统调用draw(_ rect:)前,会自动将绘制区域填充为透明,清除上一帧的所有内容,你没有主动清除不代表系统不会执行清空操作。 - 你将小球的碰撞检测、位置更新等状态变更逻辑写在了
draw(_ rect:)方法中,这不符合UIKit的设计规范:draw(_ rect:)的唯一作用是绘制当前视图的状态,不应该包含任何状态修改逻辑。系统可能在多种场景下主动触发draw(_ rect:)调用(比如视图大小变更、从遮挡状态重新露出),每次调用都会额外执行一次位置更新,直接导致小球运动速度、轨迹不符合预期,也是画面闪烁、内容偏移的核心原因。 - 你想要保留历史绘制痕迹的需求,不能直接依赖系统提供的实时绘制上下文实现,因为该上下文每次重绘都会被重置,你需要自行缓存历史绘制内容。
修复方案
基础修复(无轨迹、正常弹跳无闪烁)
- 将小球物理更新逻辑从
draw(_ rect:)中剥离,单独封装更新方法,在DisplayLink的回调中先执行状态更新,再触发重绘 - 给自定义视图设置背景色,每次重绘时系统会自动填充背景覆盖上一帧内容,避免闪烁
修改后的核心代码如下:
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
相关产品推荐
相关产品推荐

