import UIKit
import QuartzCore
import ImageIO // 新增:明确导入ImageIO框架

class GIFPlayerView: UIView {
    // MARK: - 公开属性
    /// GIF资源路径(本地)或URL(网络)
    var gifSource: String? {
        didSet {
            guard let source = gifSource else { return }
            source.hasPrefix("http") ? loadGIFFromURL(urlString: source) : loadGIFFromLocal(path: source)
        }
    }
    
    /// 当前显示的帧索引
    private(set) var currentFrameIndex: Int = 0
    
    /// 是否正在播放
    var isPlaying: Bool {
        displayLink != nil && displayLink!.isPaused == false
    }
    
    /// 动画重复次数(0表示无限循环,默认0)
    var repeatCount: Int = 0
    
    // MARK: - 私有属性
    private let imageView = UIImageView()
    private var gifFrames: [UIImage] = [] // 存储所有帧图片
    private var totalAnimationDuration: TimeInterval = 0 // 总动画时长
    private var displayLink: CADisplayLink? // 用于控制帧切换的定时器
    private var animationStartTime: CFTimeInterval = 0 // 动画启动时间
    
    // MARK: - 初始化
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupUI()
    }
    
    // MARK: - UI配置
    private func setupUI() {
        // 配置imageView
         imageView.translatesAutoresizingMaskIntoConstraints = false // 启用Auto Layout
         imageView.contentMode = .scaleAspectFill // 保持宽高比填充视图,超出部分裁剪
         imageView.clipsToBounds = true // 关键:确保超出部分被裁剪(避免内容溢出)
         addSubview(imageView)
         
         // 约束:imageView完全填充GIFPlayerView
         NSLayoutConstraint.activate([
             imageView.topAnchor.constraint(equalTo: self.topAnchor),
             imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
             imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
             imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
         ])
    }
    
    // MARK: - GIF加载
    /// 加载本地GIF
    private func loadGIFFromLocal(path: String) {
        guard let fileURL = Bundle.main.url(forResource: path, withExtension: "gif") else {
            print("❌ 本地GIF路径错误:\(path).gif")
            return
        }
        
        do {
            let gifData = try Data(contentsOf: fileURL)
            parseGIFData(data: gifData)
        } catch {
            print("❌ 加载本地GIF失败:\(error.localizedDescription)")
        }
    }
    
    /// 加载网络GIF
    private func loadGIFFromURL(urlString: String) {
        guard let url = URL(string: urlString) else {
            print("❌ 网络GIF URL无效:\(urlString)")
            return
        }
        
        DispatchQueue.global().async { [weak self] in
            do {
                let gifData = try Data(contentsOf: url)
                DispatchQueue.main.async {
                    self?.parseGIFData(data: gifData)
                }
            } catch {
                print("❌ 加载网络GIF失败:\(error.localizedDescription)")
            }
        }
    }
    
    // MARK: - GIF解析
    /// 解析GIF数据为帧数组
    private func parseGIFData(data: Data) {
        // 重置之前的状态
        stopPlay()
        gifFrames.removeAll()
        totalAnimationDuration = 0
        
        // 创建GIF图像源
        guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
            print("❌ 无法解析GIF数据")
            return
        }
        
        // 获取帧数量
        let frameCount = CGImageSourceGetCount(source)
        guard frameCount > 0 else {
            print("❌ GIF无有效帧")
            return
        }
        
        // 提取每一帧
        for i in 0..<frameCount {
            // 提取帧图像
            guard let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) else { continue }
            let frameImage = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up)
            gifFrames.append(frameImage)
            
            // 计算帧时长
            let frameDuration = self.frameDuration(for: source, at: i)
            totalAnimationDuration += frameDuration
        }
        
        // 配置初始状态
        imageView.image = gifFrames.first
        currentFrameIndex = 0
        print("🔍 初始化完成,总帧数:\(gifFrames.count),总时长:\(totalAnimationDuration)s")
        
        // 自动开始播放
        startPlay()
        
        // 打印尺寸信息
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            print("🔍 imageView尺寸:\(self.imageView.frame)")
            print("🔍 GIFPlayerView尺寸:\(self.frame)")
        }
    }
    
    /// 获取指定帧的时长
    private func frameDuration(for source: CGImageSource, at index: Int) -> TimeInterval {
        guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [CFString: Any],
              let frameProperties = properties[kCGImagePropertyGIFDictionary] as? [CFString: Any] else {
            return 0.1 // 默认帧时长
        }
        
        // 从GIF元数据获取延迟时间
        if let delayTime = frameProperties[kCGImagePropertyGIFDelayTime] as? TimeInterval {
            return delayTime < 0.01 ? 0.1 : delayTime // 过滤过短的帧时长
        }
        
        return 0.1 // 默认帧时长
    }
    
    // MARK: - 播放控制
    /// 开始播放动画
    func startPlay() {
        guard !isPlaying else { return }
        
        // 记录启动时间
        animationStartTime = CACurrentMediaTime()
        
        // 配置并启动定时器
        displayLink = CADisplayLink(target: self, selector: #selector(updateFrame))
        displayLink?.add(to: .main, forMode: .common)
    }
    
    /// 暂停播放动画
    func stopPlay() {
        guard isPlaying else { return }
        displayLink?.invalidate()
        displayLink = nil
    }
    
    /// 切换播放状态(播放/暂停)
    func togglePlayState() {
        isPlaying ? stopPlay() : startPlay()
    }
    
    /// 重置动画(从头开始)
    func resetAnimation() {
        stopPlay()
        startPlay()
    }
    
    // MARK: - 帧更新
    /// 定时更新当前显示的帧
    @objc private func updateFrame() {
        guard !gifFrames.isEmpty else { return }
        
        // 计算已播放时间
        let currentTime = CACurrentMediaTime()
        let elapsedTime = currentTime - animationStartTime
        
        // 计算每帧时长
        let frameDuration = totalAnimationDuration / Double(gifFrames.count)
        
        // 计算当前应该显示的帧索引
        var expectedIndex = Int(elapsedTime / frameDuration)
        
        // 处理重复次数
        if repeatCount > 0 {
            let totalFrames = gifFrames.count * repeatCount
            expectedIndex = min(expectedIndex, totalFrames - 1)
            if expectedIndex >= totalFrames {
                stopPlay()
                return
            }
        }
        
        // 循环播放(取模运算)
        expectedIndex %= gifFrames.count
        
        // 更新帧显示
        if expectedIndex != currentFrameIndex {
            currentFrameIndex = expectedIndex
            imageView.image = gifFrames[currentFrameIndex]
        }
    }
    
    // MARK: - 生命周期
    deinit {
        stopPlay() // 释放时停止动画
    }
}
// 1,创建变量承接GIF播放器
    let gifPlayer = GIFPlayerView()

// 2,调用播放器初始方法
    setupPlayer()

// 3,配置 GIF 播放器
    private func setupPlayer() {
        // 1. 添加播放器到页面
        view.addSubview(gifPlayer)
        // 2. 约束:GIFPlayerView 铺满整个屏幕(忽略安全区域,真正全屏)
        gifPlayer.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            // 👇 直接与 view 对齐,而非 safeAreaLayoutGuide,实现全屏(包括刘海区)
            gifPlayer.topAnchor.constraint(equalTo: view.topAnchor),
            gifPlayer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            gifPlayer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            gifPlayer.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        // 3.加载本地的gif图片,不带后缀名
        gifPlayer.gifSource = "启动页gif"
        // 4.设置播放次数(0表示无限循环)
        gifPlayer.repeatCount = 1
    }

更多推荐