Gifu/Source/Classes/Animator.swift

188 lines
6.7 KiB
Swift
Raw Normal View History

2018-02-17 15:14:53 +00:00
import UIKit
2016-10-01 11:41:22 +00:00
/// 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.
2016-10-01 11:41:22 +00:00
var frameBufferCount = 50
/// Specifies whether GIF frames should be resized.
2016-10-01 11:41:22 +00:00
var shouldResizeFrames = false
/// Responsible for loading individual frames and resizing them if necessary.
2016-11-02 01:14:02 +00:00
var frameStore: FrameStore?
/// Tracks whether the display link is initialized.
private var displayLinkInitialized: Bool = false
/// A delegate responsible for displaying the GIF frames.
2016-10-01 11:41:22 +00:00
private weak var delegate: GIFAnimatable!
/// Responsible for starting and stopping the animation.
private lazy var displayLink: CADisplayLink = { [unowned self] in
self.displayLinkInitialized = true
let display = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.onScreenUpdate))
display.isPaused = true
return display
}()
/// Introspect whether the `displayLink` is paused.
var isAnimating: Bool {
return !displayLink.isPaused
2016-04-24 09:28:09 +00:00
}
/// Total frame count of the GIF.
var frameCount: Int {
return frameStore?.frameCount ?? 0
}
2016-10-01 11:41:22 +00:00
/// Creates a new animator with a delegate.
///
/// - parameter view: A view object that implements the `GIFAnimatable` protocol.
2015-01-23 00:02:08 +00:00
///
/// - returns: A new animator instance.
2016-10-01 11:41:22 +00:00
public init(withDelegate delegate: GIFAnimatable) {
self.delegate = delegate
}
/// 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() }
2016-04-24 09:28:09 +00:00
}
}
2014-12-12 21:49:15 +00:00
/// Prepares the animator instance for animation.
2016-04-24 09:28:09 +00:00
///
2016-10-06 20:49:31 +00:00
/// - 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.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
/// - parameter completionHandler: Completion callback function
2018-09-19 11:10:33 +00:00
func prepareForAnimation(withGIFNamed imageName: String, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
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,
loopCount: loopCount,
completionHandler: completionHandler)
2016-04-24 09:28:09 +00:00
}
/// Prepares the animator instance for animation.
2016-04-24 09:28:09 +00:00
///
2016-10-06 20:49:31 +00:00
/// - 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.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
/// - parameter completionHandler: Completion callback function
2018-09-19 11:10:33 +00:00
func prepareForAnimation(withGIFData imageData: Data, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
frameStore = FrameStore(data: imageData,
size: size,
contentMode: contentMode,
framePreloadCount: frameBufferCount,
loopCount: loopCount)
frameStore!.shouldResizeFrames = shouldResizeFrames
frameStore!.prepareFrames(completionHandler)
attachDisplayLink()
2016-04-24 09:28:09 +00:00
}
/// Add the display link to the main run loop.
private func attachDisplayLink() {
2018-09-19 11:10:33 +00:00
displayLink.add(to: .main, forMode: RunLoop.Mode.common)
2016-04-24 09:28:09 +00:00
}
deinit {
if displayLinkInitialized {
displayLink.invalidate()
}
}
/// Start animating.
func startAnimating() {
if frameStore?.isAnimatable ?? false {
displayLink.isPaused = false
2016-04-24 09:28:09 +00:00
}
}
/// Stop animating.
func stopAnimating() {
displayLink.isPaused = true
2016-04-24 09:28:09 +00:00
}
/// Prepare for animation and start animating immediately.
///
2016-10-06 20:49:31 +00:00
/// - 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.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
/// - parameter completionHandler: Completion callback function
2018-09-19 11:10:33 +00:00
func animate(withGIFNamed imageName: String, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
prepareForAnimation(withGIFNamed: imageName,
size: size,
contentMode: contentMode,
loopCount: loopCount,
completionHandler: completionHandler)
startAnimating()
2016-04-24 09:28:09 +00:00
}
/// Prepare for animation and start animating immediately.
///
2016-10-06 20:49:31 +00:00
/// - 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.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
/// - parameter completionHandler: Completion callback function
2018-09-19 11:10:33 +00:00
func animate(withGIFData imageData: Data, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
prepareForAnimation(withGIFData: imageData,
size: size,
contentMode: contentMode,
loopCount: loopCount,
completionHandler: completionHandler)
startAnimating()
2016-04-24 09:28:09 +00:00
}
/// Stop animating and nullify the frame store.
func prepareForReuse() {
stopAnimating()
frameStore = nil
2016-04-24 09:28:09 +00:00
}
/// Gets the current image from the frame store.
2016-04-24 09:28:09 +00:00
///
/// - returns: An optional frame image to display.
2016-10-01 11:41:22 +00:00
func activeFrame() -> UIImage? {
return frameStore?.currentFrameImage
2016-04-24 09:28:09 +00:00
}
}
2016-04-24 09:28:09 +00:00
2016-12-16 10:40:28 +00:00
/// A proxy class to avoid a retain cycle with the display link.
fileprivate class DisplayLinkProxy {
2016-04-24 09:28:09 +00:00
/// The target animator.
private weak var target: Animator?
2016-04-24 09:28:09 +00:00
2016-10-01 11:41:22 +00:00
/// Create a new proxy object with a target animator.
///
/// - parameter target: An animator instance.
///
/// - returns: A new proxy instance.
init(target: Animator) { self.target = target }
2016-04-24 09:28:09 +00:00
/// Lets the target update the frame if needed.
@objc func onScreenUpdate() { target?.updateFrameIfNeeded() }
}