From 5f1f686eaaea52fab57454c3605fa933b2c5e214 Mon Sep 17 00:00:00 2001 From: stepajin Date: Sun, 13 Nov 2016 23:46:53 +0100 Subject: [PATCH] Add loop control (#89) * Add loop duration property * Add loop count control --- Source/Classes/Animator.swift | 34 ++++++++++++----- Source/Classes/FrameStore.swift | 60 ++++++++++++++++++++++++------ Source/Classes/GIFAnimatable.swift | 25 +++++++++---- 3 files changed, 90 insertions(+), 29 deletions(-) diff --git a/Source/Classes/Animator.swift b/Source/Classes/Animator.swift index 9aad274..39bbcbc 100644 --- a/Source/Classes/Animator.swift +++ b/Source/Classes/Animator.swift @@ -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() } diff --git a/Source/Classes/FrameStore.swift b/Source/Classes/FrameStore.swift index c8fa617..274110f 100644 --- a/Source/Classes/FrameStore.swift +++ b/Source/Classes/FrameStore.swift @@ -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. @@ -202,18 +234,22 @@ private extension FrameStore { return [Int](nextIndex.. bufferFrameCount { return } - animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(with: loadFrame(at: index)) - } + resetAnimatedFrames() + + var duration: TimeInterval = 0 + + (0.. bufferFrameCount { return } + animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(with: loadFrame(at: index)) + } + + self.loopDuration = duration } /// Reset animated frames. diff --git a/Source/Classes/GIFAnimatable.swift b/Source/Classes/GIFAnimatable.swift index cc18ed2..ecc7a67 100644 --- a/Source/Classes/GIFAnimatable.swift +++ b/Source/Classes/GIFAnimatable.swift @@ -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.