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

Swift中Lottie循环动画结束时平滑切换至非循环动画的实现

优雅实现Lottie循环动画到单次动画的平滑过渡

我之前在做Lottie动画过渡时也碰到过类似的问题,轮询realtimeAnimationProgress确实会带来不必要的CPU开销,这里给你推荐两种更高效的原生方案,完美解决“循环动画即将进入下一轮时停止并切换”的需求:

方案一:利用Lottie原生进度观察者(推荐)

Lottie提供了AnimationProgressTimeObserver来监听动画进度变化,这是官方推荐的监听方式,比主动轮询效率高得多,不会造成CPU占用上升。核心思路是:

  • 给循环动画添加进度观察者,当进度接近1.0时(比如0.99,可根据动画调整阈值)触发切换
  • 配合一个标记变量,在网络请求完成时标记需要切换,等当前循环周期接近结束时再执行操作

代码实现

import Lottie

class AnimatedButton: UIButton {
    private let animationView = LottieAnimationView()
    private var shouldSwitchToNonLoopAnimation = false
    private var progressObserver: AnimationProgressTimeObserver?
    
    // 初始化等代码省略...
    
    // 你原来的单次转循环动画方法
    func animate(animation: LottieAnimation?, endIn loop: LottieAnimation?) {
        setImage(nil, for: .normal)
        animationView.isHidden = false
        
        guard let animation = animation else { return }
        animationView.animation = animation
        animationView.loopMode = .playOnce
        animationView.play { [weak self] _ in
            guard let self = self, let loop = loop else { return }
            self.startLoopAnimation(with: loop)
        }
    }
    
    // 启动循环动画并添加进度监听
    private func startLoopAnimation(with loopAnimation: LottieAnimation) {
        animationView.animation = loopAnimation
        animationView.loopMode = .loop
        animationView.play()
        
        // 创建进度观察者
        progressObserver = AnimationProgressTimeObserver { [weak self] progress in
            guard let self = self else { return }
            // 只有标记需要切换时才处理
            guard self.shouldSwitchToNonLoopAnimation else { return }
            
            // 当进度接近循环结束(避免浮点误差,用0.99而非1.0)
            if progress >= 0.99 {
                // 移除观察者,避免内存泄漏
                self.animationView.removeProgressTimeObserver(self.progressObserver!)
                self.progressObserver = nil
                
                // 停止当前循环动画(此时已接近结尾,视觉上无突兀感)
                self.animationView.stop()
                
                // 切换到非循环动画
                self.switchToNonLoopAnimation()
            }
        }
        
        // 添加观察者到动画视图
        animationView.addProgressTimeObserver(progressObserver!, forProgressRange: 0...1)
    }
    
    // 网络请求完成时调用此方法,标记需要切换动画
    func onNetworkRequestCompleted() {
        shouldSwitchToNonLoopAnimation = true
    }
    
    // 切换到非循环动画
    private func switchToNonLoopAnimation() {
        // 替换为你的非循环动画实例
        guard let nonLoopAnimation = LottieAnimation.named("your_non_loop_animation") else { return }
        
        animationView.animation = nonLoopAnimation
        animationView.loopMode = .playOnce
        // 如果需要更平滑的衔接,可以确保新动画的首帧和循环动画的尾帧视觉匹配
        animationView.play { [weak self] _ in
            // 动画完成后恢复按钮状态等操作
            self?.resetButtonToOriginalState()
        }
    }
    
    private func resetButtonToOriginalState() {
        animationView.isHidden = true
        setImage(UIImage(named: "original_button_image"), for: .normal)
        shouldSwitchToNonLoopAnimation = false
    }
}

关键细节

  • 使用weak self避免循环引用,防止内存泄漏
  • 阈值设置为0.99而非1.0,是为了避免浮点精度问题导致错过切换时机
  • 只有当网络请求完成(shouldSwitchToNonLoopAnimation为true)时才触发切换,确保动画在当前循环周期结束前停止

方案二:基于动画时长的定时器触发

如果你的循环动画时长固定,可以提前计算单次循环的时长,在网络请求完成后,设置一个定时器,等到当前循环周期接近结束时触发切换。这种方式适合动画速度固定的场景:

private var switchTimer: Timer?

func onNetworkRequestCompleted() {
    guard let loopAnimation = animationView.animation else { return }
    // 计算当前循环剩余时长:(1 - animationView.realtimeAnimationProgress) * loopAnimation.duration / animationView.animationSpeed
    let remainingTime = (1 - animationView.realtimeAnimationProgress) * loopAnimation.duration / animationView.animationSpeed
    // 设置定时器,在剩余时长后触发切换(可以提前0.05秒,确保在循环开始前停止)
    switchTimer = Timer.scheduledTimer(withTimeInterval: remainingTime - 0.05, repeats: false) { [weak self] _ in
        self?.animationView.stop()
        self?.switchToNonLoopAnimation()
    }
}

注意事项

  • 这种方式依赖动画时长和速度的准确性,如果动画有变速逻辑,可能会出现偏差
  • 记得在适当的时机(比如按钮销毁时) invalidate 定时器,避免内存泄漏

方案对比

  • 方案一(进度观察者):原生支持、精准度高、CPU开销低,是首选方案
  • 方案二(定时器):适合简单场景,但精准度略逊于观察者

这两种方案都能避免轮询带来的CPU占用问题,实现循环动画到非循环动画的自然过渡。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 09:17:13