Core Plot 时序序列绘图连续动画切换出现空白问题求助
Core Plot 时序绘图连续动画空白问题优化方案
原有实现的核心问题根源
- 定时器触发精度不足:
Timer.scheduledTimer仅运行在默认RunLoop模式,只要界面存在交互、主线程被Core Plot的重绘任务占用,定时器就会出现明显触发延迟甚至丢帧;而原有逻辑在上一帧的不透明状态被设置为0后,才会等待下一次定时器触发加载新帧,一旦触发延迟就会出现连续的空白段。 - 动画状态管理混乱:代码中手动修改layer的
opacity属性后,叠加了removedOnCompletion = false+fillMode = forwards的补间动画,会导致模型层与实际展示层状态永久脱节,每次帧切换都会触发Core Plot全图重排,进一步挤占主线程渲染时间,拉低触发精度。 - 无效渲染负载过高:所有时间区间的plot图层全部提前挂载在图表面板上,哪怕透明度为0,Core Plot依然会对这些不可见图层执行布局计算,整体渲染负载过高,进一步拖慢帧率。
无空白流畅动画的可落地方案
1. 替换高精度动画驱动
彻底弃用普通Timer,改用CADisplayLink作为动画帧切换的驱动源:
- 将CADisplayLink配置为跟随屏幕刷新节奏触发,比如需要实现每帧展示0.5秒的效果,设置切换节奏匹配实际需求,屏幕刷新本身的高频率节奏用来做交叉淡化的平滑过渡
- 将CADisplayLink加入RunLoop的
.common模式,完全避免界面交互时出现触发延迟
2. 重构交叉淡化逻辑,从机制上消除空白间隙
调整帧切换的时序逻辑,保证整个动画流程中始终有至少一个plot处于完全不透明的展示状态:
- 切帧时先把下一帧的plot挂载到渲染层级,从opacity=0开始做淡入动画,和上一帧的淡出动画完全同步进行
- 上一帧的淡出动画完全结束后,再把对应的plot从父图层移除,不要提前把上一帧设为全透明状态
- 修正动画属性配置:所有补间动画完成后自动从layer移除,不要用
removedOnCompletion = false长期挂着动画,避免状态脱节
3. 降低不必要的渲染开销
- 不要一次性把所有时间区间的plot图层全部提前挂载到图表面板上:动画播放的过程中,离屏的plot仅保留对应的数据源配置,等即将展示前再初始化图层挂载,大幅减少Core Plot的布局计算量
- 精简每个plot的非必要元素:动画播放阶段暂时关闭网格线、动态刻度更新这类非核心的渲染逻辑,把32×32数据点的绘图区域渲染占比压缩到最低
4. 核心逻辑调整参考代码
// 动画驱动相关属性 var displayLink: CADisplayLink? var refreshCount: Int = 0 let frameHoldRefreshCount = 30 // 每帧停留30个屏幕刷新周期,对应0.5秒@60Hz刷新率 // 启动动画的入口 func startAnimation() { // 重置动画状态 animationCounter = 0 refreshCount = 0 previousAnimatedPlot = nil // 初始化CADisplayLink displayLink = CADisplayLink(target: self, selector: #selector(handleAnimationTick)) if #available(iOS 15.0, *) { displayLink?.preferredFrameRateRange = .init(minimum: 60, maximum: 60) } displayLink?.add(to: .main, forMode: .common) } @objc func handleAnimationTick() { refreshCount += 1 // 未达到当前帧的停留时长,不切帧 guard refreshCount >= frameHoldRefreshCount else { return } refreshCount = 0 // 取出即将展示的当前帧plot guard let currentPlot = graph.plot(at: animationCounter) else { animationCounter = (animationCounter + 1) % plotDetails.count return } // 先挂载新帧图层,设置初始全透明状态 graph.add(currentPlot) currentPlot.opacity = 0 // 同步启动新帧淡入动画 let fadeIn = CABasicAnimation(keyPath: "opacity") fadeIn.fromValue = 0 fadeIn.toValue = 1 fadeIn.duration = 0.15 fadeIn.isRemovedOnCompletion = true currentPlot.add(fadeIn, forKey: "fadeIn") // 同步启动上一帧淡出动画,完成后移除图层 if let previousPlot = previousAnimatedPlot { let fadeOut = CABasicAnimation(keyPath: "opacity") fadeOut.fromValue = 1 fadeOut.toValue = 0 fadeOut.duration = 0.15 fadeOut.isRemovedOnCompletion = true fadeOut.delegate = self previousPlot.add(fadeOut, forKey: "fadeOut") } previousAnimatedPlot = currentPlot animationCounter = (animationCounter + 1) % plotDetails.count } // 动画代理回调:淡出完成后移除旧图层 func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { if anim == previousAnimatedPlot?.animation(forKey: "fadeOut") { previousAnimatedPlot?.removeFromSuperlayer() } }
内容的提问来源于stack exchange,提问作者swainwri
相关产品推荐
相关产品推荐

