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

View File

@ -3,6 +3,18 @@ import ImageIO
/// Responsible for storing and updating the frames of a single GIF.
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.
let maxTimeStep = 1.0
@ -70,12 +82,13 @@ class FrameStore {
///
/// - parameter data: The raw GIF image data.
/// - 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
self.imageSource = CGImageSourceCreateWithData(data as CFData, options) ?? CGImageSourceCreateIncremental(options)
self.size = size
self.contentMode = contentMode
self.bufferFrameCount = framePreloadCount
self.loopCount = loopCount
}
// MARK: - Frames
@ -177,6 +190,11 @@ private extension FrameStore {
/// Increments the `currentFrameIndex` property.
func incrementCurrentFrameIndex() {
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.
@ -188,6 +206,20 @@ private extension FrameStore {
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.
///
/// - parameter index: Starting index.
@ -203,17 +235,21 @@ private extension FrameStore {
}
}
/// Set up animated frames after resetting them if necessary.
func setupAnimatedFrames() {
resetAnimatedFrames()
resetAnimatedFrames()
(0..<frameCount).forEach { index in
let frameDuration = CGImageFrameDuration(with: imageSource, atIndex: index)
animatedFrames += [AnimatedFrame(image: .none, duration: frameDuration)]
var duration: TimeInterval = 0
if index > bufferFrameCount { return }
animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(with: loadFrame(at: index))
}
(0..<frameCount).forEach { index in
let frameDuration = CGImageFrameDuration(with: imageSource, atIndex: index)
duration += min(frameDuration, maxTimeStep)
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.

View File

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