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

如何为ViewController中的语音按钮添加圆形进度动画?

嘿,这个需求我之前也帮人解决过,不用GIF完全可以搞定,咱们用Core Animation来实现一个可控的圆形进度指示器就行,步骤很清晰,我给你一步步拆解:

实现思路

核心思路是用CAShapeLayer绘制一个环形路径,通过修改它的strokeEnd属性控制进度,再配合CABasicAnimation实现平滑动画。这种方式是矢量绘制,缩放不失真,还能精确控制进度、自定义样式,比GIF灵活太多了。

具体实现步骤

1. 给按钮添加进度指示器层

我们可以给UIButton写个扩展,方便复用。这个扩展会帮我们创建并管理环形进度层:

extension UIButton {
    // 用关联对象存储进度层,避免重复创建
    private static let progressLayerKey = "ProgressLayerKey"
    
    var progressLayer: CAShapeLayer? {
        get {
            return objc_getAssociatedObject(self, &Self.progressLayerKey) as? CAShapeLayer
        }
        set {
            objc_setAssociatedObject(self, &Self.progressLayerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
    // 初始化进度指示器
    func setupProgressIndicator() {
        // 先移除已有层,防止重复添加
        progressLayer?.removeFromSuperlayer()
        
        let progressLayer = CAShapeLayer()
        // 计算环形路径的中心、半径(比按钮小一点,留边距)
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let radius = min(bounds.width, bounds.height) / 2 - 4
        // 绘制环形路径:从顶部开始顺时针画一圈
        let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: -CGFloat.pi/2, endAngle: CGFloat.pi * 3/2, clockwise: true)
        
        progressLayer.path = path.cgPath
        progressLayer.strokeColor = UIColor.systemBlue.cgColor // 进度条颜色
        progressLayer.fillColor = UIColor.clear.cgColor
        progressLayer.lineWidth = 3.0 // 进度条线宽
        progressLayer.strokeStart = 0.0
        progressLayer.strokeEnd = 0.0 // 初始进度为0
        progressLayer.lineCap = .round // 圆角端点,更美观
        
        layer.addSublayer(progressLayer)
        self.progressLayer = progressLayer
    }
}

2. 实现进度动画控制方法

继续在扩展里添加更新进度和重置的方法,支持动画和非动画两种模式:

extension UIButton {
    // 更新进度:progress范围0-1
    func updateProgress(to progress: CGFloat, animated: Bool = true, duration: TimeInterval = 0.3) {
        guard let progressLayer = progressLayer else {
            // 如果还没初始化进度层,先初始化再递归调用
            setupProgressIndicator()
            updateProgress(to: progress, animated: animated, duration: duration)
            return
        }
        
        // 限制进度在0-1之间,防止越界
        let targetProgress = max(0.0, min(1.0, progress))
        
        if animated {
            let animation = CABasicAnimation(keyPath: "strokeEnd")
            animation.fromValue = progressLayer.strokeEnd
            animation.toValue = targetProgress
            animation.duration = duration
            animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
            progressLayer.add(animation, forKey: "ProgressAnimation")
        }
        
        // 同步更新layer的实际值,避免动画结束后回弹
        progressLayer.strokeEnd = targetProgress
    }
    
    // 重置进度为0
    func resetProgress() {
        updateProgress(to: 0.0, animated: true)
    }
}

3. 在ViewController中使用

现在你可以在自己的ViewController里直接调用这些方法了,比如语音按钮的业务逻辑中:

class VoiceViewController: UIViewController {
    @IBOutlet weak var voiceButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 初始化进度指示器
        voiceButton.setupProgressIndicator()
    }
    
    // 模拟开始录音时的进度动画
    func startRecording() {
        // 这里可以替换成实际的业务逻辑,比如根据录音时长更新进度
        DispatchQueue.global().async {
            for i in 0...100 {
                DispatchQueue.main.async {
                    let progress = CGFloat(i) / 100.0
                    self.voiceButton.updateProgress(to: progress)
                }
                Thread.sleep(forTimeInterval: 0.05)
            }
        }
    }
    
    // 停止录音时重置进度
    func stopRecording() {
        voiceButton.resetProgress()
    }
}
额外优化建议
  • 如果你的按钮图片是圆形的,可以调整radius值让进度圈和图片完全贴合;
  • 可以自定义进度条的颜色(比如用渐变色,需要配合CAGradientLayer做mask);
  • 动画时长可以根据实际业务调整,比如录音时长是10秒,就把duration设为对应的值;
  • 不用担心进度层影响按钮交互,CAShapeLayer默认不响应触摸事件,不会干扰按钮的点击逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 08:43:12