Swift 加载gif格式的动图
·
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
}
更多推荐


所有评论(0)