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

如何用CABasicAnimation实现指定加载指示器?

如何用CABasicAnimation实现指定加载指示器?

嘿,我懂你现在的困扰——用四个独立图层来做这个加载指示器不仅代码繁琐,还容易出现动画不同步的问题,其实咱们完全可以只用一个CAShapeLayer就搞定这个效果,不用搞那么多图层。下面我给你整理了优化后的实现方案,和你想要的效果完全匹配:

核心思路

我们用单个形状图层绘制完整的圆环路径,然后通过控制strokeStart和strokeEnd属性,配合**动画组(CAAnimationGroup)**来实现分段填充的效果,再加上旋转动画让整个加载器动起来,全程只用一个图层,代码更简洁也更容易维护。

完整实现代码

private func setupLoadingIndicator() {
    // 创建单个形状图层
    let loadingLayer = CAShapeLayer()
    loadingLayer.strokeColor = UIColor.orange.cgColor
    loadingLayer.fillColor = UIColor.clear.cgColor
    loadingLayer.lineWidth = 5
    loadingLayer.lineCap = .round
    loadingLayer.strokeEnd = 0
    
    // 设置完整圆环路径(我们通过strokeStart/end控制显示分段)
    let radius = (bounds.height - loadingLayer.lineWidth) / 2
    let fullCirclePath = UIBezierPath(
        arcCenter: CGPoint(x: bounds.width/2, y: bounds.height/2),
        radius: radius,
        startAngle: CGFloat.angle(progress: 0),
        endAngle: CGFloat.angle(progress: 1),
        clockwise: true
    )
    loadingLayer.path = fullCirclePath.cgPath
    self.layer.addSublayer(loadingLayer)
    
    // 创建四段填充的动画组
    let fillAnimations = CAAnimationGroup()
    fillAnimations.duration = 4
    fillAnimations.repeatDuration = .infinity
    fillAnimations.timingFunction = CAMediaTimingFunction(name: .easeIn)
    
    var segmentAnimations = [CABasicAnimation]()
    // 为每个分段创建填充动画,设置不同的开始时间偏移
    for segmentIndex in 0..<4 {
        let startProgress = CGFloat(segmentIndex) * 0.25
        let endProgress = startProgress + 0.25
        
        let strokeStartAnim = CABasicAnimation(keyPath: "strokeStart")
        strokeStartAnim.fromValue = startProgress
        strokeStartAnim.toValue = startProgress
        
        let strokeEndAnim = CABasicAnimation(keyPath: "strokeEnd")
        strokeEndAnim.fromValue = startProgress
        strokeEndAnim.toValue = endProgress
        
        // 每个分段动画延迟1秒开始,依次触发
        strokeStartAnim.beginTime = Double(segmentIndex)
        strokeEndAnim.beginTime = Double(segmentIndex)
        
        segmentAnimations.append(strokeStartAnim)
        segmentAnimations.append(strokeEndAnim)
    }
    
    fillAnimations.animations = segmentAnimations
    loadingLayer.add(fillAnimations, forKey: "segmentFill")
    
    // 创建旋转动画,和填充动画同步
    let rotationAnim = CABasicAnimation(keyPath: "transform.rotation.z")
    rotationAnim.duration = 4
    rotationAnim.fromValue = CGFloat.angle(progress: 0)
    rotationAnim.toValue = CGFloat.angle(progress: 1) // 旋转一整圈,和四阶段填充同步
    rotationAnim.repeatDuration = .infinity
    rotationAnim.timingFunction = CAMediaTimingFunction(name: .easeIn)
    loadingLayer.add(rotationAnim, forKey: "rotation")
}

private extension CGFloat {
    static var initialAngle: CGFloat = -(.pi / 2)
    static func angle(progress: CGFloat) -> CGFloat {
        .pi * 2 * progress + .initialAngle
    }
}

关键细节解释

  • 单图层优势:只用一个CAShapeLayer,避免了多图层管理的麻烦,动画同步性更好。
  • 分段填充控制:通过strokeStart固定每个分段的起始位置,strokeEnd从起始位置动画到分段结束位置,每个分段的动画通过beginTime延迟1秒,实现依次亮起的效果。
  • 动画同步:填充动画组总时长4秒(每段1秒),旋转动画同样设置为4秒旋转一整圈,保证动画节奏完全匹配。
  • 圆角端点:lineCap = .round确保分段的两端是圆角,和你想要的视觉效果一致。

如果你想要的是填充完四段后再旋转90度循环,只需要调整旋转动画的duration为1秒,toValue设为CGFloat.angle(progress: 0.25),同时把填充动画组的duration改为1秒,四段动画的beginTime分别设为0、0.25、0.5、0.75即可,灵活调整参数就能匹配不同的动画节奏~

备注:内容来源于stack exchange,提问作者Danil Kozyr

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.23 14:02:49