如何为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
相关产品推荐
相关产品推荐

