2016-09-26 21:01:27 +00:00
|
|
|
import UIKit
|
|
|
|
import ImageIO
|
|
|
|
|
|
|
|
/// Responsible for storing and updating the frames of a single GIF.
|
|
|
|
class FrameStore {
|
|
|
|
|
|
|
|
/// Maximum duration to increment the frame timer with.
|
|
|
|
let maxTimeStep = 1.0
|
|
|
|
|
|
|
|
/// An array of animated frames from a single GIF image.
|
|
|
|
var animatedFrames = [AnimatedFrame]()
|
|
|
|
|
|
|
|
/// The target size for all frames.
|
|
|
|
let size: CGSize
|
|
|
|
|
|
|
|
/// The content mode to use when resizing.
|
|
|
|
let contentMode: UIViewContentMode
|
|
|
|
|
|
|
|
/// Maximum number of frames to load at once
|
|
|
|
let bufferFrameCount: Int
|
|
|
|
|
|
|
|
/// The total number of frames in the GIF.
|
|
|
|
var frameCount = 0
|
|
|
|
|
|
|
|
/// A reference to the original image source.
|
|
|
|
var imageSource: CGImageSource
|
|
|
|
|
|
|
|
/// The index of the current GIF frame.
|
|
|
|
var currentFrameIndex = 0 {
|
|
|
|
didSet {
|
|
|
|
previousFrameIndex = oldValue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The index of the previous GIF frame.
|
|
|
|
var previousFrameIndex = 0 {
|
|
|
|
didSet {
|
|
|
|
preloadFrameQueue.async {
|
|
|
|
self.updatePreloadedFrames()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Time elapsed since the last frame change. Used to determine when the frame should be updated.
|
|
|
|
var timeSinceLastFrameChange: TimeInterval = 0.0
|
|
|
|
|
|
|
|
/// Specifies whether GIF frames should be resized.
|
|
|
|
var shouldResizeFrames = true
|
|
|
|
|
|
|
|
/// Dispatch queue used for preloading images.
|
|
|
|
private lazy var preloadFrameQueue: DispatchQueue = {
|
|
|
|
return DispatchQueue(label: "co.kaishin.Gifu.preloadQueue")
|
|
|
|
}()
|
|
|
|
|
|
|
|
/// The current image frame to show.
|
|
|
|
var currentFrameImage: UIImage? {
|
|
|
|
return frame(at: currentFrameIndex)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The current frame duration
|
|
|
|
var currentFrameDuration: TimeInterval {
|
|
|
|
return duration(at: currentFrameIndex)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Is this image animatable?
|
|
|
|
var isAnimatable: Bool {
|
|
|
|
return imageSource.isAnimatedGIF
|
|
|
|
}
|
|
|
|
|
2016-10-01 11:41:22 +00:00
|
|
|
/// Creates an animator instance from raw GIF image data and an `Animatable` delegate.
|
2016-09-26 21:01:27 +00:00
|
|
|
///
|
|
|
|
/// - parameter data: The raw GIF image data.
|
|
|
|
/// - parameter delegate: An `Animatable` delegate.
|
|
|
|
init(data: Data, size: CGSize, contentMode: UIViewContentMode, framePreloadCount: Int) {
|
|
|
|
let options = [String(kCGImageSourceShouldCache): kCFBooleanFalse] as CFDictionary
|
|
|
|
self.imageSource = CGImageSourceCreateWithData(data as CFData, options) ?? CGImageSourceCreateIncremental(options)
|
|
|
|
self.size = size
|
|
|
|
self.contentMode = contentMode
|
|
|
|
self.bufferFrameCount = framePreloadCount
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Frames
|
|
|
|
/// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`.
|
|
|
|
func prepareFrames(_ completionHandler: ((Void) -> Void)? = .none) {
|
|
|
|
frameCount = Int(CGImageSourceGetCount(imageSource))
|
|
|
|
animatedFrames.reserveCapacity(frameCount)
|
|
|
|
preloadFrameQueue.async {
|
|
|
|
self.setupAnimatedFrames()
|
|
|
|
if let handler = completionHandler { handler() }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the frame at a particular index.
|
|
|
|
///
|
|
|
|
/// - parameter index: The index of the frame.
|
|
|
|
/// - returns: An optional image at a given frame.
|
|
|
|
func frame(at index: Int) -> UIImage? {
|
|
|
|
return animatedFrames[safe: index]?.image
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the duration at a particular index.
|
|
|
|
///
|
|
|
|
/// - parameter index: The index of the duration.
|
|
|
|
/// - returns: The duration of the given frame.
|
|
|
|
func duration(at index: Int) -> TimeInterval {
|
|
|
|
return animatedFrames[safe: index]?.duration ?? TimeInterval.infinity
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Checks whether the frame should be changed and calls a handler with the results.
|
|
|
|
///
|
|
|
|
/// - parameter duration: A `CFTimeInterval` value that will be used to determine whether frame should be changed.
|
|
|
|
/// - parameter handler: A function that takes a `Bool` and returns nothing. It will be called with the frame change result.
|
|
|
|
func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) {
|
|
|
|
incrementTimeSinceLastFrameChange(with: duration)
|
|
|
|
|
|
|
|
if currentFrameDuration > timeSinceLastFrameChange {
|
|
|
|
handler(false)
|
|
|
|
} else {
|
|
|
|
resetTimeSinceLastFrameChange()
|
|
|
|
incrementCurrentFrameIndex()
|
|
|
|
handler(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension FrameStore {
|
|
|
|
/// Whether preloading is needed or not.
|
|
|
|
var preloadingIsNeeded: Bool {
|
|
|
|
return bufferFrameCount < frameCount - 1
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Optionally loads a single frame from an image source, resizes it if required, then returns an `UIImage`.
|
|
|
|
///
|
|
|
|
/// - parameter index: The index of the frame to load.
|
|
|
|
/// - returns: An optional `UIImage` instance.
|
|
|
|
func loadFrame(at index: Int) -> UIImage? {
|
|
|
|
guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else { return .none }
|
|
|
|
let image = UIImage(cgImage: imageRef)
|
|
|
|
let scaledImage: UIImage?
|
|
|
|
|
|
|
|
if shouldResizeFrames {
|
|
|
|
switch self.contentMode {
|
|
|
|
case .scaleAspectFit: scaledImage = image.constrained(by: size)
|
|
|
|
case .scaleAspectFill: scaledImage = image.filling(size: size)
|
|
|
|
default: scaledImage = image.resized(to: size)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
scaledImage = image
|
|
|
|
}
|
|
|
|
|
|
|
|
return scaledImage
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Updates the frames by preloading new ones and replacing the previous frame with a placeholder.
|
|
|
|
func updatePreloadedFrames() {
|
|
|
|
if !preloadingIsNeeded { return }
|
|
|
|
animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex].placeholderFrame
|
|
|
|
|
|
|
|
preloadIndexes(withStartingIndex: currentFrameIndex).forEach { index in
|
|
|
|
let currentAnimatedFrame = animatedFrames[index]
|
|
|
|
if !currentAnimatedFrame.isPlaceholder { return }
|
2016-10-01 11:41:22 +00:00
|
|
|
animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(with: loadFrame(at: index))
|
2016-09-26 21:01:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Increments the `timeSinceLastFrameChange` property with a given duration.
|
|
|
|
///
|
|
|
|
/// - parameter duration: An `NSTimeInterval` value to increment the `timeSinceLastFrameChange` property with.
|
|
|
|
func incrementTimeSinceLastFrameChange(with duration: TimeInterval) {
|
|
|
|
timeSinceLastFrameChange += min(maxTimeStep, duration)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Ensures that `timeSinceLastFrameChange` remains accurate after each frame change by substracting the `currentFrameDuration`.
|
|
|
|
func resetTimeSinceLastFrameChange() {
|
|
|
|
timeSinceLastFrameChange -= currentFrameDuration
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Increments the `currentFrameIndex` property.
|
|
|
|
func incrementCurrentFrameIndex() {
|
2016-10-01 11:41:22 +00:00
|
|
|
currentFrameIndex = increment(frameIndex: currentFrameIndex)
|
2016-09-26 21:01:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Increments a given frame index, taking into account the `frameCount` and looping when necessary.
|
|
|
|
///
|
|
|
|
/// - parameter index: The `Int` value to increment.
|
|
|
|
/// - parameter byValue: The `Int` value to increment with.
|
|
|
|
/// - returns: A new `Int` value.
|
2016-10-01 11:41:22 +00:00
|
|
|
func increment(frameIndex: Int, by value: Int = 1) -> Int {
|
|
|
|
return (frameIndex + value) % frameCount
|
2016-09-26 21:01:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the indexes of the frames to preload based on a starting frame index.
|
|
|
|
///
|
|
|
|
/// - parameter index: Starting index.
|
|
|
|
/// - returns: An array of indexes to preload.
|
|
|
|
func preloadIndexes(withStartingIndex index: Int) -> [Int] {
|
2016-10-01 11:41:22 +00:00
|
|
|
let nextIndex = increment(frameIndex: index)
|
|
|
|
let lastIndex = increment(frameIndex: index, by: bufferFrameCount)
|
2016-09-26 21:01:27 +00:00
|
|
|
|
|
|
|
if lastIndex >= nextIndex {
|
|
|
|
return [Int](nextIndex...lastIndex)
|
|
|
|
} else {
|
|
|
|
return [Int](nextIndex..<frameCount) + [Int](0...lastIndex)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set up animated frames after resetting them if necessary.
|
|
|
|
func setupAnimatedFrames() {
|
|
|
|
resetAnimatedFrames()
|
|
|
|
|
|
|
|
(0..<frameCount).forEach { index in
|
|
|
|
let frameDuration = CGImageFrameDuration(with: imageSource, atIndex: index)
|
|
|
|
animatedFrames += [AnimatedFrame(image: .none, duration: frameDuration)]
|
|
|
|
|
|
|
|
if index > bufferFrameCount { return }
|
2016-10-01 11:41:22 +00:00
|
|
|
animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(with: loadFrame(at: index))
|
2016-09-26 21:01:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Reset animated frames.
|
|
|
|
func resetAnimatedFrames() {
|
|
|
|
animatedFrames = []
|
|
|
|
}
|
|
|
|
}
|