186 lines
6.7 KiB
Swift
186 lines
6.7 KiB
Swift
/// 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
|
|
|
|
/// Specifies whether GIF frames should be resized.
|
|
var shouldResizeFrames = false
|
|
|
|
/// Responsible for loading individual frames and resizing them if necessary.
|
|
var frameStore: FrameStore?
|
|
|
|
/// Tracks whether the display link is initialized.
|
|
private var displayLinkInitialized: Bool = false
|
|
|
|
/// A delegate responsible for displaying the GIF frames.
|
|
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
|
|
}
|
|
|
|
/// Total frame count of the GIF.
|
|
var frameCount: Int {
|
|
return frameStore?.frameCount ?? 0
|
|
}
|
|
|
|
/// Creates a new animator with a delegate.
|
|
///
|
|
/// - parameter view: A view object that implements the `GIFAnimatable` protocol.
|
|
///
|
|
/// - returns: A new animator instance.
|
|
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() }
|
|
}
|
|
}
|
|
|
|
/// Prepares the animator instance for animation.
|
|
///
|
|
/// - 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
|
|
func prepareForAnimation(withGIFNamed imageName: String, size: CGSize, contentMode: UIViewContentMode, 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)
|
|
}
|
|
|
|
/// Prepares the animator instance for animation.
|
|
///
|
|
/// - 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
|
|
func prepareForAnimation(withGIFData imageData: Data, size: CGSize, contentMode: UIViewContentMode, 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()
|
|
}
|
|
|
|
/// Add the display link to the main run loop.
|
|
private func attachDisplayLink() {
|
|
displayLink.add(to: .main, forMode: RunLoopMode.commonModes)
|
|
}
|
|
|
|
deinit {
|
|
if displayLinkInitialized {
|
|
displayLink.invalidate()
|
|
}
|
|
}
|
|
|
|
/// Start animating.
|
|
func startAnimating() {
|
|
if frameStore?.isAnimatable ?? false {
|
|
displayLink.isPaused = false
|
|
}
|
|
}
|
|
|
|
/// Stop animating.
|
|
func stopAnimating() {
|
|
displayLink.isPaused = true
|
|
}
|
|
|
|
/// Prepare for animation and start animating immediately.
|
|
///
|
|
/// - 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
|
|
func animate(withGIFNamed imageName: String, size: CGSize, contentMode: UIViewContentMode, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
|
|
prepareForAnimation(withGIFNamed: imageName,
|
|
size: size,
|
|
contentMode: contentMode,
|
|
loopCount: loopCount,
|
|
completionHandler: completionHandler)
|
|
startAnimating()
|
|
}
|
|
|
|
/// Prepare for animation and start animating immediately.
|
|
///
|
|
/// - 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
|
|
func animate(withGIFData imageData: Data, size: CGSize, contentMode: UIViewContentMode, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
|
|
prepareForAnimation(withGIFData: imageData,
|
|
size: size,
|
|
contentMode: contentMode,
|
|
loopCount: loopCount,
|
|
completionHandler: completionHandler)
|
|
startAnimating()
|
|
}
|
|
|
|
/// Stop animating and nullify the frame store.
|
|
func prepareForReuse() {
|
|
stopAnimating()
|
|
frameStore = nil
|
|
}
|
|
|
|
/// Gets the current image from the frame store.
|
|
///
|
|
/// - returns: An optional frame image to display.
|
|
func activeFrame() -> UIImage? {
|
|
return frameStore?.currentFrameImage
|
|
}
|
|
}
|
|
|
|
/// A proxy class to avoid a retain cycle with the display link.
|
|
fileprivate class DisplayLinkProxy {
|
|
|
|
/// The target animator.
|
|
private weak var target: Animator?
|
|
|
|
/// 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 }
|
|
|
|
/// Lets the target update the frame if needed.
|
|
@objc func onScreenUpdate() { target?.updateFrameIfNeeded() }
|
|
}
|