2 // AnimatableImageView.swift
5 // Created by bl4ckra1sond3tre on 4/22/16.
7 // The AnimatableImageView, AnimatedFrame and Animator is a modified version of
8 // some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu)
10 // The MIT License (MIT)
12 // Copyright (c) 2018 Reda Lemeden.
14 // Permission is hereby granted, free of charge, to any person obtaining a copy of
15 // this software and associated documentation files (the "Software"), to deal in
16 // the Software without restriction, including without limitation the rights to
17 // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
18 // the Software, and to permit persons to whom the Software is furnished to do so,
19 // subject to the following conditions:
21 // The above copyright notice and this permission notice shall be included in all
22 // copies or substantial portions of the Software.
24 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
26 // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
27 // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
28 // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
29 // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 // The name and characters used in the demo of this software are property of their
37 /// Protocol of `AnimatedImageView`.
38 public protocol AnimatedImageViewDelegate: class {
40 Called after the animatedImageView has finished each animation loop.
42 - parameter imageView: The animatedImageView that is being animated.
43 - parameter count: The looped count.
45 func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt)
48 Called after the animatedImageView has reached the max repeat count.
50 - parameter imageView: The animatedImageView that is being animated.
52 func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView)
55 extension AnimatedImageViewDelegate {
56 public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {}
57 public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {}
60 /// `AnimatedImageView` is a subclass of `UIImageView` for displaying animated image.
61 open class AnimatedImageView: UIImageView {
63 /// Proxy object for prevending a reference cycle between the CADDisplayLink and AnimatedImageView.
65 private weak var target: AnimatedImageView?
67 init(target: AnimatedImageView) {
71 @objc func onScreenUpdate() {
76 /// Enumeration that specifies repeat count of GIF
77 public enum RepeatCount: Equatable {
79 case finite(count: UInt)
82 public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool {
84 case let (.finite(l), .finite(r)):
87 (.infinite, .infinite):
97 // MARK: - Public property
98 /// Whether automatically play the animation when the view become visible. Default is true.
99 public var autoPlayAnimatedImage = true
101 /// The size of the frame cache.
102 public var framePreloadCount = 10
104 /// Specifies whether the GIF frames should be pre-scaled to save memory. Default is true.
105 public var needsPrescaling = true
107 /// The animation timer's run loop mode. Default is `NSRunLoopCommonModes`. Set this property to `NSDefaultRunLoopMode` will make the animation pause during UIScrollView scrolling.
108 public var runLoopMode = RunLoopMode.commonModes {
110 if runLoopMode == newValue {
114 displayLink.remove(from: .main, forMode: runLoopMode)
115 displayLink.add(to: .main, forMode: newValue)
121 /// The repeat count.
122 public var repeatCount = RepeatCount.infinite {
124 if oldValue != repeatCount {
127 layer.setNeedsDisplay()
132 /// Delegate of this `AnimatedImageView` object. See `AnimatedImageViewDelegate` protocol for more.
133 public weak var delegate: AnimatedImageViewDelegate?
135 // MARK: - Private property
136 /// `Animator` instance that holds the frames of a specific image in memory.
137 private var animator: Animator?
139 /// A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. :D
140 private var isDisplayLinkInitialized: Bool = false
142 /// A display link that keeps calling the `updateFrame` method on every screen refresh.
143 private lazy var displayLink: CADisplayLink = {
144 self.isDisplayLinkInitialized = true
145 let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
146 displayLink.add(to: .main, forMode: self.runLoopMode)
147 displayLink.isPaused = true
152 override open var image: Image? {
154 if image != oldValue {
158 layer.setNeedsDisplay()
163 if isDisplayLinkInitialized {
164 displayLink.invalidate()
168 override open var isAnimating: Bool {
169 if isDisplayLinkInitialized {
170 return !displayLink.isPaused
172 return super.isAnimating
176 /// Starts the animation.
177 override open func startAnimating() {
178 if self.isAnimating {
181 if animator?.isReachMaxRepeatCount ?? false {
185 displayLink.isPaused = false
189 /// Stops the animation.
190 override open func stopAnimating() {
191 super.stopAnimating()
192 if isDisplayLinkInitialized {
193 displayLink.isPaused = true
197 override open func display(_ layer: CALayer) {
198 if let currentFrame = animator?.currentFrame {
199 layer.contents = currentFrame.cgImage
201 layer.contents = image?.cgImage
205 override open func didMoveToWindow() {
206 super.didMoveToWindow()
210 override open func didMoveToSuperview() {
211 super.didMoveToSuperview()
215 // This is for back compatibility that using regular UIImageView to show animated image.
216 override func shouldPreloadAllAnimation() -> Bool {
220 // MARK: - Private method
221 /// Reset the animator.
222 private func reset() {
224 if let imageSource = image?.kf.imageSource?.imageRef {
225 animator = Animator(imageSource: imageSource,
226 contentMode: contentMode,
228 framePreloadCount: framePreloadCount,
229 repeatCount: repeatCount)
230 animator?.delegate = self
231 animator?.needsPrescaling = needsPrescaling
232 animator?.prepareFramesAsynchronously()
237 private func didMove() {
238 if autoPlayAnimatedImage && animator != nil {
239 if let _ = superview, let _ = window {
247 /// Update the current frame with the displayLink duration.
248 private func updateFrame() {
249 let duration: CFTimeInterval
251 // CA based display link is opt-out from ProMotion by default.
252 // So the duration and its FPS might not match.
253 // See [#718](https://github.com/onevcat/Kingfisher/issues/718)
254 if #available(iOS 10.0, tvOS 10.0, *) {
255 // By setting CADisableMinimumFrameDuration to YES in Info.plist may
256 // cause the preferredFramesPerSecond being 0
257 if displayLink.preferredFramesPerSecond == 0 {
258 duration = displayLink.duration
260 // Some devices (like iPad Pro 10.5) will have a different FPS.
261 duration = 1.0 / Double(displayLink.preferredFramesPerSecond)
264 duration = displayLink.duration
267 if animator?.updateCurrentFrame(duration: duration) ?? false {
268 layer.setNeedsDisplay()
270 if animator?.isReachMaxRepeatCount ?? false {
272 delegate?.animatedImageViewDidFinishAnimating(self)
278 extension AnimatedImageView: AnimatorDelegate {
279 func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) {
280 delegate?.animatedImageView(self, didPlayAnimationLoops: count)
284 /// Keeps a reference to an `Image` instance and its duration as a GIF frame.
285 struct AnimatedFrame {
287 let duration: TimeInterval
289 static let null: AnimatedFrame = AnimatedFrame(image: .none, duration: 0.0)
292 protocol AnimatorDelegate: class {
293 func animator(_ animator: Animator, didPlayAnimationLoops count: UInt)
298 // MARK: Private property
299 fileprivate let size: CGSize
300 fileprivate let maxFrameCount: Int
301 fileprivate let imageSource: CGImageSource
302 fileprivate let maxRepeatCount: AnimatedImageView.RepeatCount
304 fileprivate var animatedFrames = [AnimatedFrame]()
305 fileprivate let maxTimeStep: TimeInterval = 1.0
306 fileprivate var frameCount = 0
307 fileprivate var currentFrameIndex = 0
308 fileprivate var currentFrameIndexInBuffer = 0
309 fileprivate var currentPreloadIndex = 0
310 fileprivate var timeSinceLastFrameChange: TimeInterval = 0.0
311 fileprivate var needsPrescaling = true
312 fileprivate var currentRepeatCount: UInt = 0
313 fileprivate weak var delegate: AnimatorDelegate?
315 /// Loop count of animated image.
316 private var loopCount = 0
318 var currentFrame: UIImage? {
319 return frame(at: currentFrameIndexInBuffer)
322 var isReachMaxRepeatCount: Bool {
323 switch maxRepeatCount {
325 return currentRepeatCount >= 1
326 case .finite(let maxCount):
327 return currentRepeatCount >= maxCount
333 var contentMode = UIViewContentMode.scaleToFill
335 private lazy var preloadQueue: DispatchQueue = {
336 return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
340 Init an animator with image source reference.
342 - parameter imageSource: The reference of animated image.
343 - parameter contentMode: Content mode of AnimatedImageView.
344 - parameter size: Size of AnimatedImageView.
345 - parameter framePreloadCount: Frame cache size.
347 - returns: The animator object.
349 init(imageSource source: CGImageSource,
350 contentMode mode: UIViewContentMode,
352 framePreloadCount count: Int,
353 repeatCount: AnimatedImageView.RepeatCount) {
354 self.imageSource = source
355 self.contentMode = mode
357 self.maxFrameCount = count
358 self.maxRepeatCount = repeatCount
361 func frame(at index: Int) -> Image? {
362 return animatedFrames[safe: index]?.image
365 func prepareFramesAsynchronously() {
366 preloadQueue.async { [weak self] in
367 self?.prepareFrames()
371 private func prepareFrames() {
372 frameCount = CGImageSourceGetCount(imageSource)
374 if let properties = CGImageSourceCopyProperties(imageSource, nil),
375 let gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
376 let loopCount = gifInfo[kCGImagePropertyGIFLoopCount as String] as? Int
378 self.loopCount = loopCount
381 let frameToProcess = min(frameCount, maxFrameCount)
382 animatedFrames.reserveCapacity(frameToProcess)
383 animatedFrames = (0..<frameToProcess).reduce([]) { $0 + pure(prepareFrame(at: $1))}
384 currentPreloadIndex = (frameToProcess + 1) % frameCount - 1
387 private func prepareFrame(at index: Int) -> AnimatedFrame {
389 guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else {
390 return AnimatedFrame.null
393 let defaultGIFFrameDuration = 0.100
394 let frameDuration = imageSource.kf.gifProperties(at: index).map {
397 let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as Double?
398 let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as Double?
399 let duration = unclampedDelayTime ?? delayTime ?? 0.0
402 http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp
403 Many annoying ads specify a 0 duration to make an image flash as quickly as
404 possible. We follow Safari and Firefox's behavior and use a duration of 100 ms
405 for any frames that specify a duration of <= 10 ms.
406 See <rdar://problem/7689300> and <http://webkit.org/b/36082> for more information.
408 See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser.
410 return duration > 0.011 ? duration : defaultGIFFrameDuration
411 } ?? defaultGIFFrameDuration
413 let image = Image(cgImage: imageRef)
414 let scaledImage: Image?
417 scaledImage = image.kf.resize(to: size, for: contentMode)
422 return AnimatedFrame(image: scaledImage, duration: frameDuration)
426 Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`.
428 func updateCurrentFrame(duration: CFTimeInterval) -> Bool {
429 timeSinceLastFrameChange += min(maxTimeStep, duration)
430 guard let frameDuration = animatedFrames[safe: currentFrameIndexInBuffer]?.duration, frameDuration <= timeSinceLastFrameChange else {
434 timeSinceLastFrameChange -= frameDuration
436 let lastFrameIndex = currentFrameIndexInBuffer
437 currentFrameIndexInBuffer += 1
438 currentFrameIndexInBuffer = currentFrameIndexInBuffer % animatedFrames.count
440 if animatedFrames.count < frameCount {
441 preloadFrameAsynchronously(at: lastFrameIndex)
444 currentFrameIndex += 1
446 if currentFrameIndex == frameCount {
447 currentFrameIndex = 0
448 currentRepeatCount += 1
450 delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)
456 private func preloadFrameAsynchronously(at index: Int) {
457 preloadQueue.async { [weak self] in
458 self?.preloadFrame(at: index)
462 private func preloadFrame(at index: Int) {
463 animatedFrames[index] = prepareFrame(at: currentPreloadIndex)
464 currentPreloadIndex += 1
465 currentPreloadIndex = currentPreloadIndex % frameCount
469 extension CGImageSource: KingfisherCompatible { }
470 extension Kingfisher where Base: CGImageSource {
471 func gifProperties(at index: Int) -> [String: Double]? {
472 let properties = CGImageSourceCopyPropertiesAtIndex(base, index, nil) as Dictionary?
473 return properties?[kCGImagePropertyGIFDictionary] as? [String: Double]
478 fileprivate subscript(safe index: Int) -> Element? {
479 return indices ~= index ? self[index] : nil
483 private func pure<T>(_ value: T) -> [T] {