Add loop control (#89)

* Add loop duration property
* Add loop count control
This commit is contained in:
stepajin 2016-11-13 23:46:53 +01:00 committed by Reda Lemeden
parent 3bede018a9
commit 5f1f686eaa
3 changed files with 90 additions and 29 deletions

View File

@ -1,6 +1,11 @@
/// Responsible for parsing GIF data and decoding the individual frames. /// Responsible for parsing GIF data and decoding the individual frames.
public class Animator { public class Animator {
/// Total duration of one animation loop
var loopDuration: TimeInterval {
return frameStore?.loopDuration ?? 0
}
/// Number of frame to buffer. /// Number of frame to buffer.
var frameBufferCount = 50 var frameBufferCount = 50
@ -46,6 +51,11 @@ public class Animator {
/// Checks if there is a new frame to display. /// Checks if there is a new frame to display.
fileprivate func updateFrameIfNeeded() { fileprivate func updateFrameIfNeeded() {
guard let store = frameStore else { return } guard let store = frameStore else { return }
if store.isFinished {
stopAnimating()
return
}
store.shouldChangeFrame(with: displayLink.duration) { store.shouldChangeFrame(with: displayLink.duration) {
if $0 { delegate.animatorHasNewFrame() } if $0 { delegate.animatorHasNewFrame() }
} }
@ -56,12 +66,14 @@ public class Animator {
/// - parameter imageName: The file name of the GIF in the main bundle. /// - parameter imageName: The file name of the GIF in the main bundle.
/// - parameter size: The target size of the individual frames. /// - parameter size: The target size of the individual frames.
/// - parameter contentMode: The view content mode to use for the individual frames. /// - parameter contentMode: The view content mode to use for the individual frames.
func prepareForAnimation(withGIFNamed imageName: String, size: CGSize, contentMode: UIViewContentMode) { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
/// - parameter completionHandler: Completion callback function
func prepareForAnimation(withGIFNamed imageName: String, size: CGSize, contentMode: UIViewContentMode, loopCount: Int = 0, completionHandler: ((Void) -> Void)? = .none) {
guard let extensionRemoved = imageName.components(separatedBy: ".")[safe: 0], guard let extensionRemoved = imageName.components(separatedBy: ".")[safe: 0],
let imagePath = Bundle.main.url(forResource: extensionRemoved, withExtension: "gif"), let imagePath = Bundle.main.url(forResource: extensionRemoved, withExtension: "gif"),
let data = try? Data(contentsOf: imagePath) else { return } let data = try? Data(contentsOf: imagePath) else { return }
prepareForAnimation(withGIFData: data, size: size, contentMode: contentMode) prepareForAnimation(withGIFData: data, size: size, contentMode: contentMode, loopCount: loopCount, completionHandler: completionHandler)
} }
/// Prepares the animator instance for animation. /// Prepares the animator instance for animation.
@ -69,10 +81,12 @@ public class Animator {
/// - parameter imageData: GIF image data. /// - parameter imageData: GIF image data.
/// - parameter size: The target size of the individual frames. /// - parameter size: The target size of the individual frames.
/// - parameter contentMode: The view content mode to use for the individual frames. /// - parameter contentMode: The view content mode to use for the individual frames.
func prepareForAnimation(withGIFData imageData: Data, size: CGSize, contentMode: UIViewContentMode) { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
frameStore = FrameStore(data: imageData, size: size, contentMode: contentMode, framePreloadCount: frameBufferCount) /// - parameter completionHandler: Completion callback function
func prepareForAnimation(withGIFData imageData: Data, size: CGSize, contentMode: UIViewContentMode, loopCount: Int = 0, completionHandler: ((Void) -> Void)? = .none) {
frameStore = FrameStore(data: imageData, size: size, contentMode: contentMode, framePreloadCount: frameBufferCount, loopCount: loopCount)
frameStore?.shouldResizeFrames = shouldResizeFrames frameStore?.shouldResizeFrames = shouldResizeFrames
frameStore?.prepareFrames() frameStore?.prepareFrames(completionHandler)
attachDisplayLink() attachDisplayLink()
} }
@ -105,8 +119,9 @@ public class Animator {
/// - parameter imageName: The file name of the GIF in the main bundle. /// - parameter imageName: The file name of the GIF in the main bundle.
/// - parameter size: The target size of the individual frames. /// - parameter size: The target size of the individual frames.
/// - parameter contentMode: The view content mode to use for the individual frames. /// - parameter contentMode: The view content mode to use for the individual frames.
func animate(withGIFNamed imageName: String, size: CGSize, contentMode: UIViewContentMode) { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
prepareForAnimation(withGIFNamed: imageName, size: size, contentMode: contentMode) func animate(withGIFNamed imageName: String, size: CGSize, contentMode: UIViewContentMode, loopCount: Int = 0) {
prepareForAnimation(withGIFNamed: imageName, size: size, contentMode: contentMode, loopCount: loopCount)
startAnimating() startAnimating()
} }
@ -115,8 +130,9 @@ public class Animator {
/// - parameter imageData: GIF image data. /// - parameter imageData: GIF image data.
/// - parameter size: The target size of the individual frames. /// - parameter size: The target size of the individual frames.
/// - parameter contentMode: The view content mode to use for the individual frames. /// - parameter contentMode: The view content mode to use for the individual frames.
func animate(withGIFData imageData: Data, size: CGSize, contentMode: UIViewContentMode) { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
prepareForAnimation(withGIFData: imageData, size: size, contentMode: contentMode) func animate(withGIFData imageData: Data, size: CGSize, contentMode: UIViewContentMode, loopCount: Int = 0) {
prepareForAnimation(withGIFData: imageData, size: size, contentMode: contentMode, loopCount: loopCount)
startAnimating() startAnimating()
} }

View File

@ -3,6 +3,18 @@ import ImageIO
/// Responsible for storing and updating the frames of a single GIF. /// Responsible for storing and updating the frames of a single GIF.
class FrameStore { class FrameStore {
/// Total duration of one animation loop
var loopDuration: TimeInterval = 0
/// Flag indicating if number of loops has been reached
var isFinished: Bool = false
/// Desired number of loops, <= 0 for infinite loop
let loopCount: Int
/// Index of current loop
var currentLoop = 0
/// Maximum duration to increment the frame timer with. /// Maximum duration to increment the frame timer with.
let maxTimeStep = 1.0 let maxTimeStep = 1.0
@ -70,12 +82,13 @@ class FrameStore {
/// ///
/// - parameter data: The raw GIF image data. /// - parameter data: The raw GIF image data.
/// - parameter delegate: An `Animatable` delegate. /// - parameter delegate: An `Animatable` delegate.
init(data: Data, size: CGSize, contentMode: UIViewContentMode, framePreloadCount: Int) { init(data: Data, size: CGSize, contentMode: UIViewContentMode, framePreloadCount: Int, loopCount: Int) {
let options = [String(kCGImageSourceShouldCache): kCFBooleanFalse] as CFDictionary let options = [String(kCGImageSourceShouldCache): kCFBooleanFalse] as CFDictionary
self.imageSource = CGImageSourceCreateWithData(data as CFData, options) ?? CGImageSourceCreateIncremental(options) self.imageSource = CGImageSourceCreateWithData(data as CFData, options) ?? CGImageSourceCreateIncremental(options)
self.size = size self.size = size
self.contentMode = contentMode self.contentMode = contentMode
self.bufferFrameCount = framePreloadCount self.bufferFrameCount = framePreloadCount
self.loopCount = loopCount
} }
// MARK: - Frames // MARK: - Frames
@ -177,6 +190,11 @@ private extension FrameStore {
/// Increments the `currentFrameIndex` property. /// Increments the `currentFrameIndex` property.
func incrementCurrentFrameIndex() { func incrementCurrentFrameIndex() {
currentFrameIndex = increment(frameIndex: currentFrameIndex) currentFrameIndex = increment(frameIndex: currentFrameIndex)
if isLastLoop(loopIndex: currentLoop) && isLastFrame(frameIndex: currentFrameIndex) {
isFinished = true
} else if currentFrameIndex == 0 {
currentLoop = currentLoop + 1
}
} }
/// Increments a given frame index, taking into account the `frameCount` and looping when necessary. /// Increments a given frame index, taking into account the `frameCount` and looping when necessary.
@ -188,6 +206,20 @@ private extension FrameStore {
return (frameIndex + value) % frameCount return (frameIndex + value) % frameCount
} }
/// Indicates if current frame is the last one.
/// - parameter frameIndex: Index of current frame.
/// - returns: True if current frame is the last one.
func isLastFrame(frameIndex: Int) -> Bool {
return frameIndex == frameCount - 1
}
/// Indicates if current loop is the last one. Always false for infinite loops.
/// - parameter loopIndex: Index of current loop.
/// - returns: True if current loop is the last one.
func isLastLoop(loopIndex: Int) -> Bool {
return loopIndex == loopCount - 1
}
/// Returns the indexes of the frames to preload based on a starting frame index. /// Returns the indexes of the frames to preload based on a starting frame index.
/// ///
/// - parameter index: Starting index. /// - parameter index: Starting index.
@ -202,18 +234,22 @@ private extension FrameStore {
return [Int](nextIndex..<frameCount) + [Int](0...lastIndex) return [Int](nextIndex..<frameCount) + [Int](0...lastIndex)
} }
} }
/// Set up animated frames after resetting them if necessary.
func setupAnimatedFrames() { func setupAnimatedFrames() {
resetAnimatedFrames() resetAnimatedFrames()
(0..<frameCount).forEach { index in var duration: TimeInterval = 0
let frameDuration = CGImageFrameDuration(with: imageSource, atIndex: index)
animatedFrames += [AnimatedFrame(image: .none, duration: frameDuration)] (0..<frameCount).forEach { index in
let frameDuration = CGImageFrameDuration(with: imageSource, atIndex: index)
if index > bufferFrameCount { return } duration += min(frameDuration, maxTimeStep)
animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(with: loadFrame(at: index)) animatedFrames += [AnimatedFrame(image: .none, duration: frameDuration)]
}
if index > bufferFrameCount { return }
animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(with: loadFrame(at: index))
}
self.loopDuration = duration
} }
/// Reset animated frames. /// Reset animated frames.

View File

@ -29,6 +29,11 @@ extension GIFAnimatable where Self: ImageContainer {
} }
extension GIFAnimatable { extension GIFAnimatable {
/// Total duration of one animation loop
public var gifLoopDuration: TimeInterval {
return animator?.loopDuration ?? 0
}
/// Returns the active frame if available. /// Returns the active frame if available.
public var activeFrame: UIImage? { public var activeFrame: UIImage? {
return animator?.activeFrame() return animator?.activeFrame()
@ -47,33 +52,37 @@ extension GIFAnimatable {
/// Prepare for animation and start animating immediately. /// Prepare for animation and start animating immediately.
/// ///
/// - parameter imageName: The file name of the GIF in the main bundle. /// - parameter imageName: The file name of the GIF in the main bundle.
public func animate(withGIFNamed imageName: String) { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
animator?.animate(withGIFNamed: imageName, size: frame.size, contentMode: contentMode) public func animate(withGIFNamed imageName: String, loopCount: Int = 0) {
animator?.animate(withGIFNamed: imageName, size: frame.size, contentMode: contentMode, loopCount: loopCount)
} }
/// Prepare for animation and start animating immediately. /// Prepare for animation and start animating immediately.
/// ///
/// - parameter imageData: GIF image data. /// - parameter imageData: GIF image data.
public func animate(withGIFData imageData: Data) { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
animator?.animate(withGIFData: imageData, size: frame.size, contentMode: contentMode) public func animate(withGIFData imageData: Data, loopCount: Int = 0) {
animator?.animate(withGIFData: imageData, size: frame.size, contentMode: contentMode, loopCount: loopCount)
} }
/// Prepares the animator instance for animation. /// Prepares the animator instance for animation.
/// ///
/// - parameter imageName: The file name of the GIF in the main bundle. /// - parameter imageName: The file name of the GIF in the main bundle.
public func prepareForAnimation(withGIFNamed imageName: String) { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
animator?.prepareForAnimation(withGIFNamed: imageName, size: frame.size, contentMode: contentMode) public func prepareForAnimation(withGIFNamed imageName: String, loopCount: Int = 0, completionHandler: ((Void) -> Void)? = .none) {
animator?.prepareForAnimation(withGIFNamed: imageName, size: frame.size, contentMode: contentMode, loopCount: loopCount, completionHandler: completionHandler)
} }
/// Prepare for animation and start animating immediately. /// Prepare for animation and start animating immediately.
/// ///
/// - parameter imageData: GIF image data. /// - parameter imageData: GIF image data.
public func prepareForAnimation(withGIFData imageData: Data) { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
public func prepareForAnimation(withGIFData imageData: Data, loopCount: Int = 0, completionHandler: ((Void) -> Void)? = .none) {
if var imageContainer = self as? ImageContainer { if var imageContainer = self as? ImageContainer {
imageContainer.image = UIImage(data: imageData) imageContainer.image = UIImage(data: imageData)
} }
animator?.prepareForAnimation(withGIFData: imageData, size: frame.size, contentMode: contentMode) animator?.prepareForAnimation(withGIFData: imageData, size: frame.size, contentMode: contentMode, loopCount: loopCount, completionHandler: completionHandler)
} }
/// Stop animating and free up GIF data from memory. /// Stop animating and free up GIF data from memory.