CABasicAnimation完成速度远快于Timer的问题求助
嘿,我仔细看了你的代码,终于搞清楚为什么进度条总是提前填满了,而且时长越长差异越大——核心是路径角度设置错了,还有动画逻辑和倒计时反了,另外还有个布局时机的小坑,咱们一步步来解决:
问题根源
圆形路径的角度范围不对
你创建的UIBezierPath用了startAngle: -CGFloat.pi/2和endAngle: 2 * CGFloat.pi,这意味着路径的总角度是2π - (-π/2) = 5π/2(也就是450度,相当于1.25个完整的圆)。而strokeEnd从0到1对应的是整个路径的长度,所以当动画执行到总时长的80%时(360/450=0.8),进度条就已经画完一圈了,看起来填满了,但Timer还剩20%的时间没走完。比如你设30秒的话,进度条24秒就满了,Timer还要再走6秒,时长越长这个差值就越大。动画逻辑和倒计时反了
你的Timer是倒计时(从设定时长减到0),但动画却是从空(strokeEnd=0)填充到满(strokeEnd=1),这不仅不符合用户对倒计时进度条的直觉,也加深了视觉上的“不同步”错觉。布局时机错误
在viewDidLoad里用timerContainer.center创建路径,这时候AutoLayout还没完成,timerContainer的实际位置和你拿到的center可能不一致,导致路径位置偏移,看起来也会怪怪的。
修复步骤
1. 修正圆形路径的角度
把路径的endAngle改成3 * CGFloat.pi/2,这样从-π/2(底部)顺时针到3π/2(底部),总角度正好是2π(360度),对应完整的一个圆:
let circularPath = UIBezierPath(arcCenter: timerContainer.center, radius: 35, startAngle: -CGFloat.pi / 2, endAngle: 3 * CGFloat.pi / 2, clockwise: true)
2. 调整动画方向匹配倒计时
- 初始时把
shapeLayer.strokeEnd设为1(进度条满格) - 动画的
toValue改成0,让进度条随着时间逐渐清空:
// 在viewDidLayoutSubviews里修改(后续会移到这里) shapeLayer.strokeEnd = 1 // 在start()方法里修改动画 let basicAnimation = CABasicAnimation(keyPath: "strokeEnd") basicAnimation.toValue = 0 // 从满到空,匹配倒计时逻辑 basicAnimation.fillMode = .forwards basicAnimation.duration = CFTimeInterval(minutes * 60 + seconds) basicAnimation.isRemovedOnCompletion = false shapeLayer.add(basicAnimation, forKey: "begin")
3. 修复路径位置偏移问题
把创建路径和添加图层的代码从viewDidLoad移到viewDidLayoutSubviews,这个方法在AutoLayout完成布局后调用,能保证timerContainer.center是正确的位置。另外要把trackLayer改成类属性,避免重复添加:
// 类属性添加trackLayer let shapeLayer = CAShapeLayer() let trackLayer = CAShapeLayer() override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 先移除旧图层,避免重复添加 trackLayer.removeFromSuperlayer() shapeLayer.removeFromSuperlayer() let circularPath = UIBezierPath(arcCenter: timerContainer.center, radius: 35, startAngle: -CGFloat.pi / 2, endAngle: 3 * CGFloat.pi / 2, clockwise: true) trackLayer.path = circularPath.cgPath trackLayer.strokeColor = UIColor.lightGray.cgColor trackLayer.lineWidth = 5 trackLayer.fillColor = UIColor.clear.cgColor shapeLayer.path = circularPath.cgPath shapeLayer.strokeColor = UIColor.red.cgColor shapeLayer.lineWidth = 5 shapeLayer.fillColor = UIColor.clear.cgColor shapeLayer.strokeEnd = 1 shapeLayer.lineCap = .round view.layer.addSublayer(trackLayer) view.layer.addSublayer(shapeLayer) }
4. 可选:优化Timer精度(解决延迟问题)
Timer的1秒触发间隔不是绝对精确的,当有UI交互时可能会延迟。如果需要更高精度的同步,可以用CADisplayLink结合时间戳来计算剩余时间:
// 替换原有的Timer相关代码 var startTime: Date? var displayLink: CADisplayLink? @objc func updateTimer() { guard let startTime = startTime else { return } let elapsed = Date().timeIntervalSince(startTime) let totalTime = Double(minutes * 60 + seconds) let remaining = totalTime - elapsed if remaining <= 0 { displayLink?.invalidate() minutes = 0 seconds = 0 } else { minutes = Int(remaining / 60) seconds = Int(remaining.truncatingRemainder(dividingBy: 60)) } displayTimer() } @IBAction func start() { startTime = Date() displayLink = CADisplayLink(target: self, selector: #selector(updateTimer)) displayLink?.add(to: .main, forMode: .common) // 动画代码不变 let basicAnimation = CABasicAnimation(keyPath: "strokeEnd") basicAnimation.toValue = 0 basicAnimation.fillMode = .forwards basicAnimation.duration = CFTimeInterval(minutes * 60 + seconds) basicAnimation.isRemovedOnCompletion = false shapeLayer.add(basicAnimation, forKey: "begin") }
效果验证
修改完成后,进度条会从满格开始,随着倒计时逐渐清空,动画时长和倒计时总时长完全匹配,不会再出现提前填满的情况,路径位置也会精准对应timerContainer的中心。
内容的提问来源于stack exchange,提问作者Andy_V

