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.
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue