Add loop control (#89)
* Add loop duration property * Add loop count control
This commit is contained in:
parent
3bede018a9
commit
5f1f686eaa
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -203,17 +235,21 @@ private extension FrameStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set up animated frames after resetting them if necessary.
|
|
||||||
func setupAnimatedFrames() {
|
func setupAnimatedFrames() {
|
||||||
resetAnimatedFrames()
|
resetAnimatedFrames()
|
||||||
|
|
||||||
|
var duration: TimeInterval = 0
|
||||||
|
|
||||||
(0..<frameCount).forEach { index in
|
(0..<frameCount).forEach { index in
|
||||||
let frameDuration = CGImageFrameDuration(with: imageSource, atIndex: index)
|
let frameDuration = CGImageFrameDuration(with: imageSource, atIndex: index)
|
||||||
|
duration += min(frameDuration, maxTimeStep)
|
||||||
animatedFrames += [AnimatedFrame(image: .none, duration: frameDuration)]
|
animatedFrames += [AnimatedFrame(image: .none, duration: frameDuration)]
|
||||||
|
|
||||||
if index > bufferFrameCount { return }
|
if index > bufferFrameCount { return }
|
||||||
animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(with: loadFrame(at: index))
|
animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(with: loadFrame(at: index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.loopDuration = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset animated frames.
|
/// Reset animated frames.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue