如何实现无闪烁的双向UIBezierPath描边动画加载器?
解决方案
一、现有双图层方案的无闪烁修复
你的双图层方案可以实现无闪烁,问题出在动画结束后未同步图层的模型状态与展示状态,以及切换时的初始状态设置逻辑错误。具体修改步骤如下:
1. 动画结束时同步模型层状态
动画完成后,必须将图层的strokeStart和strokeEnd设置为动画结束时**展示层(presentation layer)**的当前值,而非硬编码固定值。这样能避免模型层与展示层状态不一致导致的跳变闪烁。
修改后的动画触发代码:
// 向左动画 func animateLeft() { CATransaction.begin() CATransaction.setCompletionBlock { [weak self] in guard let self = self, let presentationLayer = self.barLayerLeft.presentation() else { return } // 同步模型层到动画结束的展示层状态 CATransaction.begin() CATransaction.setDisableActions(true) self.barLayerLeft.strokeStart = presentationLayer.strokeStart self.barLayerLeft.strokeEnd = presentationLayer.strokeEnd // 设置右侧图层初始状态,与左侧结束状态衔接 self.barLayerRight.strokeStart = 0 self.barLayerRight.strokeEnd = 0.01 CATransaction.commit() // 触发反向动画 self.animateRight() } self.barLayerLeft.add(self.groupedAnimation, forKey: "groupLeft") CATransaction.commit() } // 向右动画 func animateRight() { CATransaction.begin() CATransaction.setCompletionBlock { [weak self] in guard let self = self, let presentationLayer = self.barLayerRight.presentation() else { return } // 同步模型层到动画结束的展示层状态 CATransaction.begin() CATransaction.setDisableActions(true) self.barLayerRight.strokeStart = presentationLayer.strokeStart self.barLayerRight.strokeEnd = presentationLayer.strokeEnd // 设置左侧图层初始状态,与右侧结束状态衔接 self.barLayerLeft.strokeStart = 0 self.barLayerLeft.strokeEnd = 0.01 CATransaction.commit() // 触发反向动画 self.animateLeft() } self.barLayerRight.add(self.groupedAnimation, forKey: "groupRight") CATransaction.commit() }
2. 初始化图层的初始状态
确保两个图层初始化时状态一致,避免初始显示异常:
// 初始化图层状态 CATransaction.begin() CATransaction.setDisableActions(true) self.barLayerLeft.strokeStart = 0 self.barLayerLeft.strokeEnd = 0.01 self.barLayerRight.strokeStart = 0 self.barLayerRight.strokeEnd = 0.01 CATransaction.commit()
二、更优的单图层实现方案
双图层切换容易出现状态同步问题,推荐使用单个CAShapeLayer实现,通过控制strokeStart和strokeEnd的往返动画,彻底避免图层切换的闪烁问题。
实现思路
- 始终使用同一条从左到右的路径
- 正向动画:
strokeEnd从0.01到1,strokeStart从0到0.99(延迟0.3秒触发) - 反向动画:
strokeEnd从0.99到0.01,strokeStart从1到0(延迟0.3秒触发) - 用动画组配合completion block实现无限循环
完整代码示例
class BarLoaderView: UIView { private let barLayer = CAShapeLayer() private let animationDuration: Double = 1.3 private let delayDuration: Double = 0.3 override init(frame: CGRect) { super.init(frame: frame) setupLayer() startAnimation() } required init?(coder: NSCoder) { super.init(coder: coder) setupLayer() startAnimation() } private func setupLayer() { let path = UIBezierPath() path.move(to: CGPoint(x: 0, y: bounds.midY)) path.addLine(to: CGPoint(x: bounds.width, y: bounds.midY)) barLayer.path = path.cgPath barLayer.lineWidth = 4 barLayer.lineCap = .round barLayer.strokeColor = UIColor.systemBlue.cgColor barLayer.strokeStart = 0 barLayer.strokeEnd = 0.01 // 保留小段可见区域 layer.addSublayer(barLayer) } private func startAnimation() { animateForward() } private func animateForward() { // strokeEnd动画:从0.01到1 let strokeEndAnim = CABasicAnimation(keyPath: "strokeEnd") strokeEndAnim.fromValue = 0.01 strokeEndAnim.toValue = 1 strokeEndAnim.duration = animationDuration strokeEndAnim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) // strokeStart动画:从0到0.99,延迟触发 let strokeStartAnim = CABasicAnimation(keyPath: "strokeStart") strokeStartAnim.fromValue = 0 strokeStartAnim.toValue = 0.99 strokeStartAnim.duration = animationDuration - delayDuration strokeStartAnim.beginTime = delayDuration strokeStartAnim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) let group = CAAnimationGroup() group.animations = [strokeEndAnim, strokeStartAnim] group.duration = animationDuration CATransaction.begin() CATransaction.setCompletionBlock { [weak self] in self?.animateBackward() } barLayer.add(group, forKey: "forward") // 同步模型层状态到动画结束值 CATransaction.setDisableActions(true) barLayer.strokeStart = 0.99 barLayer.strokeEnd = 1 CATransaction.commit() } private func animateBackward() { // strokeEnd动画:从0.99到0.01 let strokeEndAnim = CABasicAnimation(keyPath: "strokeEnd") strokeEndAnim.fromValue = 0.99 strokeEndAnim.toValue = 0.01 strokeEndAnim.duration = animationDuration strokeEndAnim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) // strokeStart动画:从1到0,延迟触发 let strokeStartAnim = CABasicAnimation(keyPath: "strokeStart") strokeStartAnim.fromValue = 1 strokeStartAnim.toValue = 0 strokeStartAnim.duration = animationDuration - delayDuration strokeStartAnim.beginTime = delayDuration strokeStartAnim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) let group = CAAnimationGroup() group.animations = [strokeEndAnim, strokeStartAnim] group.duration = animationDuration CATransaction.begin() CATransaction.setCompletionBlock { [weak self] in self?.animateForward() } barLayer.add(group, forKey: "backward") // 同步模型层状态到动画结束值 CATransaction.setDisableActions(true) barLayer.strokeStart = 0 barLayer.strokeEnd = 0.01 CATransaction.commit() } }
方案优势
- 无需切换图层,彻底避免闪烁问题
- 状态同步逻辑更简单,仅需维护单个图层
- 动画循环更流畅,符合目标加载器的视觉效果
内容的提问来源于stack exchange,提问作者Bob de Graaf
相关产品推荐
相关产品推荐

