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

如何实现类似UIActivityIndicatorView的自定义旋转闪烁动画指示器

我来帮你把这个自定义活动指示器的实现补全,要达到和UIActivityIndicatorView一样的旋转闪烁效果,我们需要完善绘制逻辑、组合动画这几块内容,直接看完整实现和说明:

自定义旋转闪烁活动指示器实现

这个实现核心是多个色块按顺序闪烁+视图整体旋转的组合动画,和系统Indicator的效果对齐,同时保留可配置性。

1. 完整代码实现

import UIKit

class MyIndicator: UIView {
    // 可配置参数,根据需求调整
    private let segmentCount = 12 // 指示器的色块数量
    private let segmentColor: UIColor = .systemBlue // 色块颜色
    private let animationDuration: CFTimeInterval = 1.2 // 一轮闪烁动画时长
    private let gap: CGFloat = .pi/4 / 6.0 // 每个色块之间的间隙角度
    
    // 保存动画组,用于后续停止动画时清理
    private var animationGroups: [CAAnimationGroup] = []
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupBaseConfig()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupBaseConfig()
    }
    
    // 初始化基础配置
    private func setupBaseConfig() {
        backgroundColor = .clear
        isHidden = true // 默认隐藏,启动动画时再显示
    }
    
    // 绘制指示器的所有色块
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        // 色块半径,留边距避免超出视图范围
        let radius = min(bounds.width, bounds.height) / 2 * 0.7
        // 计算每个色块的角度(总圆周减去所有间隙后平分)
        let segmentAngle = (2 * .pi - CGFloat(segmentCount) * gap) / CGFloat(segmentCount)
        
        for i in 0..<segmentCount {
            let startAngle = CGFloat(i) * (segmentAngle + gap)
            let endAngle = startAngle + segmentAngle
            
            // 绘制单个扇形色块
            let path = UIBezierPath(arcCenter: center,
                                    radius: radius,
                                    startAngle: startAngle,
                                    endAngle: endAngle,
                                    clockwise: true)
            segmentColor.setFill()
            path.fill()
        }
    }
    
    // 启动动画(包含闪烁+旋转)
    func startAnimating() {
        isHidden = false
        cleanExistingAnimations() // 先清理旧动画,避免重复叠加
        
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let radius = min(bounds.width, bounds.height) / 2 * 0.7
        let segmentAngle = (2 * .pi - CGFloat(segmentCount) * gap) / CGFloat(segmentCount)
        
        // 给每个色块单独创建图层和闪烁动画
        for i in 0..<segmentCount {
            let startAngle = CGFloat(i) * (segmentAngle + gap)
            let endAngle = startAngle + segmentAngle
            
            let segmentLayer = CAShapeLayer()
            segmentLayer.path = UIBezierPath(arcCenter: center,
                                             radius: radius,
                                             startAngle: startAngle,
                                             endAngle: endAngle,
                                             clockwise: true).cgPath
            segmentLayer.fillColor = segmentColor.cgColor
            layer.addSublayer(segmentLayer)
            
            // 透明度动画:从淡到亮再回到淡,模拟闪烁
            let opacityAnim = CAKeyframeAnimation(keyPath: "opacity")
            opacityAnim.values = [0.1, 1.0, 0.1]
            opacityAnim.keyTimes = [0, 0.5, 1]
            opacityAnim.duration = animationDuration
            opacityAnim.repeatCount = .infinity
            
            // 每个色块延迟启动动画,实现顺序闪烁的效果
            let delay = (animationDuration / Double(segmentCount)) * Double(i)
            opacityAnim.beginTime = CACurrentMediaTime() + delay
            
            // 动画组统一管理
            let animGroup = CAAnimationGroup()
            animGroup.animations = [opacityAnim]
            animGroup.duration = animationDuration
            animGroup.repeatCount = .infinity
            animGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
            
            segmentLayer.add(animGroup, forKey: "blinkAnimation")
            animationGroups.append(animGroup)
        }
        
        // 添加整体旋转动画
        let rotateAnim = CABasicAnimation(keyPath: "transform.rotation.z")
        rotateAnim.fromValue = 0
        rotateAnim.toValue = 2 * .pi
        rotateAnim.duration = animationDuration * 2 // 旋转速度可调整
        rotateAnim.repeatCount = .infinity
        layer.add(rotateAnim, forKey: "rotateAnimation")
    }
    
    // 停止动画并清理资源
    func stopAnimating() {
        isHidden = true
        cleanExistingAnimations()
    }
    
    // 清理所有动画和子图层
    private func cleanExistingAnimations() {
        layer.removeAllAnimations()
        layer.sublayers?.forEach { $0.removeAllAnimations() }
        layer.sublayers = nil
        animationGroups.removeAll()
    }
}

2. 使用方式

和系统的UIActivityIndicatorView用法一致,非常简单:

// 创建指示器(设置合适的大小)
let customIndicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
customIndicator.center = view.center
view.addSubview(customIndicator)

// 启动动画
customIndicator.startAnimating()

// 停止动画(比如网络请求完成后)
// customIndicator.stopAnimating()

3. 可自定义调整的点

  • 修改segmentCount可以调整色块的数量,系统默认是12个
  • 修改segmentColor可以替换指示器的颜色
  • 调整animationDuration可以改变动画的快慢
  • 修改gap可以调整色块之间的间隙大小

内容的提问来源于stack exchange,提问作者jpulikkottil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 11:14:17