You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现无闪烁的双向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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 23:55:54