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

如何实现shimmer动画从一个UIView流向另一个UIView的连续循环效果

实现方案

1. 先优化ShimmerView基础实现

首先修复原实现重复添加梯度层的问题,同时支持单次动画结束回调,方便串联下一段动画:

class ShimmerView: UIView {
    private let gradientColorOne: CGColor = UIColor.purple.cgColor
    private let gradientColorTwo: CGColor = UIColor.yellow.cgColor
    // 把梯度层设为成员变量,避免重复添加
    private var gradientLayer: CAGradientLayer?

    private func addGradientLayer() -> CAGradientLayer {
        // 已添加过就直接返回,避免重复创建
        if let existLayer = gradientLayer {
            return existLayer
        }
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = self.bounds
        gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
        gradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
        gradientLayer.colors = [gradientColorOne, gradientColorTwo, gradientColorOne]
        gradientLayer.locations = [0.0, 0.5, 1.0]
        self.layer.addSublayer(gradientLayer)
        self.gradientLayer = gradientLayer
        return gradientLayer
    }

    private func addAnimation() -> CABasicAnimation {
        let animation = CABasicAnimation(keyPath: "locations")
        animation.fromValue = [-1.0, -0.5, 0.0]
        animation.toValue = [1.0, 1.5, 2.0]
        // 不要设置无限重复,改成单次动画
        animation.repeatCount = 1
        animation.duration = 2
        // 动画结束后保持最终状态,避免闪回
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false
        return animation
    }

    // 新增带完成回调的启动方法
    func startAnimating(completion: @escaping () -> Void) {
        let gradientLayer = addGradientLayer()
        let animation = addAnimation()
        
        CATransaction.begin()
        CATransaction.setCompletionBlock { [weak self] in
            // 动画结束后重置梯度层状态,为下一次播放做准备
            self?.gradientLayer?.removeAllAnimations()
            self?.gradientLayer?.locations = [0.0, 0.5, 1.0]
            completion()
        }
        gradientLayer.add(animation, forKey: animation.keyPath)
        CATransaction.commit()
    }

    // 新增停止方法,页面退出时调用避免内存泄漏
    func stopAnimating() {
        gradientLayer?.removeAllAnimations()
        gradientLayer?.removeFromSuperlayer()
        gradientLayer = nil
    }
}

2. 实现视图间的动画序列循环

在父视图控制器中编写序列触发逻辑,通过动画结束回调串联两个视图的播放顺序,自动循环:

override func viewDidLoad() {
    super.viewDidLoad()
    view1.backgroundColor = .purple
    view2.backgroundColor = .purple
    // 启动循环序列
    startShimmerSequence()
}

// 动画序列控制方法
private func startShimmerSequence() {
    // 先播放view1的动画
    view1.startAnimating { [weak self] in
        guard let self = self else { return }
        // view1播放结束后播放view2的动画
        self.view2.startAnimating { [weak self] in
            guard let self = self else { return }
            // view2播放结束后重新触发序列,实现无限循环
            self.startShimmerSequence()
        }
    }
}

// 页面消失时停止动画,避免内存泄漏
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    view1.stopAnimating()
    view2.stopAnimating()
}

方案原理

原固定延迟方案的问题是动画执行的时间存在系统级的微小偏差,多次循环后偏差会累积导致时序错位。改用动画完成回调触发下一段动画的方式,每一轮的播放完全依赖上一段的结束信号,不会出现累积误差,能保证动画序列永远严格按照view1播放→view2播放→view1播放的逻辑循环。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.15 10:03:00