init, finished
This commit is contained in:
179
GIFCollector MessagesExtension/Views/GIFPlayerView.swift
Normal file
179
GIFCollector MessagesExtension/Views/GIFPlayerView.swift
Normal file
@@ -0,0 +1,179 @@
|
||||
import UIKit
|
||||
import ImageIO
|
||||
|
||||
class GIFPlayerView: UIView {
|
||||
private var imageView: UIImageView!
|
||||
private var displayLink: CADisplayLink?
|
||||
private var imageSource: CGImageSource?
|
||||
private var frameCount: Int = 0
|
||||
private var currentFrameIndex: Int = 0
|
||||
private var frameDurations: [TimeInterval] = []
|
||||
private var totalDuration: TimeInterval = 0
|
||||
private var currentTime: TimeInterval = 0
|
||||
private var previousTimestamp: TimeInterval = 0
|
||||
|
||||
// Default configuration
|
||||
private var loopCount: Int = 0 // 0 means loop forever
|
||||
private var currentLoopCount: Int = 0
|
||||
|
||||
// Public properties
|
||||
var isPlaying: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopAnimating()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.clipsToBounds = true
|
||||
addSubview(imageView)
|
||||
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
func loadGIF(from data: Data) {
|
||||
stopAnimating()
|
||||
|
||||
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return }
|
||||
imageSource = source
|
||||
|
||||
// Get frame count
|
||||
frameCount = CGImageSourceGetCount(source)
|
||||
guard frameCount > 0 else { return }
|
||||
|
||||
// Calculate frame durations
|
||||
frameDurations.removeAll()
|
||||
totalDuration = 0
|
||||
|
||||
for i in 0..<frameCount {
|
||||
let duration = frameDurationAtIndex(i)
|
||||
frameDurations.append(duration)
|
||||
totalDuration += duration
|
||||
}
|
||||
|
||||
// Reset state
|
||||
currentFrameIndex = 0
|
||||
currentTime = 0
|
||||
|
||||
// Display first frame
|
||||
if let image = imageAtIndex(0) {
|
||||
imageView.image = image
|
||||
}
|
||||
}
|
||||
|
||||
func startAnimating() {
|
||||
guard !isPlaying, frameCount > 1 else { return }
|
||||
|
||||
isPlaying = true
|
||||
previousTimestamp = CACurrentMediaTime()
|
||||
|
||||
displayLink = CADisplayLink(target: self, selector: #selector(updateFrame))
|
||||
displayLink?.add(to: .main, forMode: .common)
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
isPlaying = false
|
||||
displayLink?.invalidate()
|
||||
displayLink = nil
|
||||
}
|
||||
|
||||
func loadGIF(from url: URL, completion: @escaping (Bool) -> Void) {
|
||||
URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self, let data = data, error == nil else {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.loadGIF(from: data)
|
||||
completion(true)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
@objc private func updateFrame(displayLink: CADisplayLink) {
|
||||
let timestamp = CACurrentMediaTime()
|
||||
let elapsed = timestamp - previousTimestamp
|
||||
previousTimestamp = timestamp
|
||||
|
||||
currentTime += elapsed
|
||||
|
||||
// Check if we need to show the next frame
|
||||
while currentTime >= frameDurations[currentFrameIndex], frameCount > 0 {
|
||||
currentTime -= frameDurations[currentFrameIndex]
|
||||
currentFrameIndex = (currentFrameIndex + 1) % frameCount
|
||||
|
||||
// Handle loop count
|
||||
if currentFrameIndex == 0 && loopCount > 0 {
|
||||
currentLoopCount += 1
|
||||
if currentLoopCount >= loopCount {
|
||||
stopAnimating()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Update the image
|
||||
if let image = imageAtIndex(currentFrameIndex) {
|
||||
imageView.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func imageAtIndex(_ index: Int) -> UIImage? {
|
||||
guard let source = imageSource, index < frameCount,
|
||||
let cgImage = CGImageSourceCreateImageAtIndex(source, index, nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
||||
private func frameDurationAtIndex(_ index: Int) -> TimeInterval {
|
||||
guard let source = imageSource, index < frameCount else {
|
||||
return 0.1 // Default duration
|
||||
}
|
||||
|
||||
// Get frame properties
|
||||
guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [String: Any],
|
||||
let gifProperties = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] else {
|
||||
return 0.1 // Default duration
|
||||
}
|
||||
|
||||
// Get delay time
|
||||
var delayTime: TimeInterval = 0.1 // Default duration
|
||||
|
||||
if let unclampedDelay = gifProperties[kCGImagePropertyGIFUnclampedDelayTime as String] as? TimeInterval,
|
||||
unclampedDelay > 0 {
|
||||
delayTime = unclampedDelay
|
||||
} else if let delay = gifProperties[kCGImagePropertyGIFDelayTime as String] as? TimeInterval,
|
||||
delay > 0 {
|
||||
delayTime = delay
|
||||
}
|
||||
|
||||
// Clamp to minimum delay (ensures reasonable frame rate)
|
||||
return max(delayTime, 0.02)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user