AnimatedImageView.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. //
  2. // AnimatableImageView.swift
  3. // Kingfisher
  4. //
  5. // Created by bl4ckra1sond3tre on 4/22/16.
  6. // Copyright © 2016 Wei Wang. All rights reserved.
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy
  9. // of this software and associated documentation files (the "Software"), to deal
  10. // in the Software without restriction, including without limitation the rights
  11. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  12. // copies of the Software, and to permit persons to whom the Software is
  13. // furnished to do so, subject to the following conditions:
  14. //
  15. // The above copyright notice and this permission notice shall be included in
  16. // all copies or substantial portions of the Software.
  17. //
  18. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  24. // THE SOFTWARE.
  25. import UIKit
  26. import ImageIO
  27. /// `AnimatedImageView` is a subclass of `UIImageView` for displaying animated image.
  28. public class AnimatedImageView: UIImageView {
  29. /// Proxy object for prevending a reference cycle between the CADDisplayLink and AnimatedImageView.
  30. class TargetProxy {
  31. private weak var target: AnimatedImageView?
  32. init(target: AnimatedImageView) {
  33. self.target = target
  34. }
  35. @objc func onScreenUpdate() {
  36. target?.updateFrame()
  37. }
  38. }
  39. // MARK: - Public property
  40. /// Whether automatically play the animation when the view become visible. Default is true.
  41. public var autoPlayAnimatedImage = true
  42. /// The size of the frame cache.
  43. public var framePreloadCount = 10
  44. /// Specifies whether the GIF frames should be pre-scaled to save memory. Default is true.
  45. public var needsPrescaling = true
  46. /// The animation timer's run loop mode. Default is `NSRunLoopCommonModes`. Set this property to `NSDefaultRunLoopMode` will make the animation pause during UIScrollView scrolling.
  47. public var runLoopMode = NSRunLoopCommonModes {
  48. willSet {
  49. if runLoopMode == newValue {
  50. return
  51. } else {
  52. stopAnimating()
  53. displayLink.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: runLoopMode)
  54. displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: newValue)
  55. startAnimating()
  56. }
  57. }
  58. }
  59. // MARK: - Private property
  60. /// `Animator` instance that holds the frames of a specific image in memory.
  61. private var animator: Animator?
  62. /// A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. :D
  63. private var displayLinkInitialized: Bool = false
  64. /// A display link that keeps calling the `updateFrame` method on every screen refresh.
  65. private lazy var displayLink: CADisplayLink = {
  66. self.displayLinkInitialized = true
  67. let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
  68. displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: self.runLoopMode)
  69. displayLink.paused = true
  70. return displayLink
  71. }()
  72. // MARK: - Override
  73. override public var image: Image? {
  74. didSet {
  75. if image != oldValue {
  76. reset()
  77. }
  78. setNeedsDisplay()
  79. layer.setNeedsDisplay()
  80. }
  81. }
  82. deinit {
  83. if displayLinkInitialized {
  84. displayLink.invalidate()
  85. }
  86. }
  87. override public func isAnimating() -> Bool {
  88. if displayLinkInitialized {
  89. return !displayLink.paused
  90. } else {
  91. return super.isAnimating()
  92. }
  93. }
  94. /// Starts the animation.
  95. override public func startAnimating() {
  96. if self.isAnimating() {
  97. return
  98. } else {
  99. displayLink.paused = false
  100. }
  101. }
  102. /// Stops the animation.
  103. override public func stopAnimating() {
  104. super.stopAnimating()
  105. if displayLinkInitialized {
  106. displayLink.paused = true
  107. }
  108. }
  109. override public func displayLayer(layer: CALayer) {
  110. if let currentFrame = animator?.currentFrame {
  111. layer.contents = currentFrame.CGImage
  112. } else {
  113. layer.contents = image?.CGImage
  114. }
  115. }
  116. override public func didMoveToWindow() {
  117. super.didMoveToWindow()
  118. didMove()
  119. }
  120. override public func didMoveToSuperview() {
  121. super.didMoveToSuperview()
  122. didMove()
  123. }
  124. // MARK: - Private method
  125. /// Reset the animator.
  126. private func reset() {
  127. animator = nil
  128. if let imageSource = image?.kf_imageSource?.imageRef {
  129. animator = Animator(imageSource: imageSource, contentMode: contentMode, size: bounds.size, framePreloadCount: framePreloadCount)
  130. animator?.needsPrescaling = needsPrescaling
  131. animator?.prepareFrames()
  132. }
  133. didMove()
  134. }
  135. private func didMove() {
  136. if autoPlayAnimatedImage && animator != nil {
  137. if let _ = superview, _ = window {
  138. startAnimating()
  139. } else {
  140. stopAnimating()
  141. }
  142. }
  143. }
  144. /// Update the current frame with the displayLink duration.
  145. private func updateFrame() {
  146. if animator?.updateCurrentFrame(displayLink.duration) ?? false {
  147. layer.setNeedsDisplay()
  148. }
  149. }
  150. }
  151. /// Keeps a reference to an `Image` instance and its duration as a GIF frame.
  152. struct AnimatedFrame {
  153. var image: Image?
  154. let duration: NSTimeInterval
  155. static func null() -> AnimatedFrame {
  156. return AnimatedFrame(image: .None, duration: 0.0)
  157. }
  158. }
  159. // MARK: - Animator
  160. ///
  161. class Animator {
  162. // MARK: Private property
  163. private let size: CGSize
  164. private let maxFrameCount: Int
  165. private let imageSource: CGImageSourceRef
  166. private var animatedFrames = [AnimatedFrame]()
  167. private let maxTimeStep: NSTimeInterval = 1.0
  168. private var frameCount = 0
  169. private var currentFrameIndex = 0
  170. private var currentPreloadIndex = 0
  171. private var timeSinceLastFrameChange: NSTimeInterval = 0.0
  172. private var needsPrescaling = true
  173. /// Loop count of animatd image.
  174. private var loopCount = 0
  175. var currentFrame: UIImage? {
  176. return frameAtIndex(currentFrameIndex)
  177. }
  178. var contentMode: UIViewContentMode = .ScaleToFill
  179. /**
  180. Init an animator with image source reference.
  181. - parameter imageSource: The reference of animated image.
  182. - parameter contentMode: Content mode of AnimatedImageView.
  183. - parameter size: Size of AnimatedImageView.
  184. - framePreloadCount: Frame cache size.
  185. - returns: The animator object.
  186. */
  187. init(imageSource src: CGImageSourceRef, contentMode mode: UIViewContentMode, size: CGSize, framePreloadCount: Int) {
  188. self.imageSource = src
  189. self.contentMode = mode
  190. self.size = size
  191. self.maxFrameCount = framePreloadCount
  192. }
  193. func frameAtIndex(index: Int) -> Image? {
  194. return animatedFrames[index].image
  195. }
  196. func prepareFrames() {
  197. frameCount = CGImageSourceGetCount(imageSource)
  198. if let properties = CGImageSourceCopyProperties(imageSource, nil),
  199. gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
  200. loopCount = gifInfo[kCGImagePropertyGIFLoopCount as String] as? Int {
  201. self.loopCount = loopCount
  202. }
  203. let frameToProcess = min(frameCount, maxFrameCount)
  204. animatedFrames.reserveCapacity(frameToProcess)
  205. animatedFrames = (0..<frameToProcess).reduce([]) { $0 + pure(prepareFrame($1))}
  206. }
  207. func prepareFrame(index: Int) -> AnimatedFrame {
  208. guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else {
  209. return AnimatedFrame.null()
  210. }
  211. let frameDuration = imageSource.kf_GIFPropertiesAtIndex(index).flatMap { (gifInfo) -> Double? in
  212. let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as Double?
  213. let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as Double?
  214. let duration = unclampedDelayTime ?? delayTime
  215. /**
  216. http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp
  217. Many annoying ads specify a 0 duration to make an image flash as quickly as
  218. possible. We follow Safari and Firefox's behavior and use a duration of 100 ms
  219. for any frames that specify a duration of <= 10 ms.
  220. See <rdar://problem/7689300> and <http://webkit.org/b/36082> for more information.
  221. See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser.
  222. */
  223. return duration > 0.011 ? duration : 0.100
  224. }
  225. let image = Image(CGImage: imageRef)
  226. let scaledImage: Image?
  227. if needsPrescaling {
  228. scaledImage = image.kf_resizeToSize(size, contentMode: contentMode)
  229. } else {
  230. scaledImage = image
  231. }
  232. return AnimatedFrame(image: scaledImage, duration: frameDuration ?? 0.0)
  233. }
  234. /**
  235. Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`.
  236. */
  237. func updateCurrentFrame(duration: CFTimeInterval) -> Bool {
  238. timeSinceLastFrameChange += min(maxTimeStep, duration)
  239. guard let frameDuration = animatedFrames[safe: currentFrameIndex]?.duration where frameDuration <= timeSinceLastFrameChange else {
  240. return false
  241. }
  242. timeSinceLastFrameChange -= frameDuration
  243. let lastFrameIndex = currentFrameIndex
  244. currentFrameIndex += 1
  245. currentFrameIndex = currentFrameIndex % animatedFrames.count
  246. if animatedFrames.count < frameCount {
  247. animatedFrames[lastFrameIndex] = prepareFrame(currentPreloadIndex)
  248. currentPreloadIndex += 1
  249. currentPreloadIndex = currentPreloadIndex % frameCount
  250. }
  251. return true
  252. }
  253. }
  254. // MARK: - Resize
  255. extension Image {
  256. func kf_resizeToSize(size: CGSize, contentMode: UIViewContentMode) -> Image {
  257. switch contentMode {
  258. case .ScaleAspectFit:
  259. let newSize = self.size.kf_sizeConstrainedSize(size)
  260. return kf_resizeToSize(newSize)
  261. case .ScaleAspectFill:
  262. let newSize = self.size.kf_sizeFillingSize(size)
  263. return kf_resizeToSize(newSize)
  264. default:
  265. return kf_resizeToSize(size)
  266. }
  267. }
  268. private func kf_resizeToSize(size: CGSize) -> Image {
  269. UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
  270. drawInRect(CGRect(origin: CGPoint.zero, size: size))
  271. let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
  272. UIGraphicsEndImageContext()
  273. return resizedImage ?? self
  274. }
  275. }
  276. extension CGSize {
  277. func kf_sizeConstrainedSize(size: CGSize) -> CGSize {
  278. let aspectWidth = round(kf_aspectRatio * size.height)
  279. let aspectHeight = round(size.width / kf_aspectRatio)
  280. return aspectWidth > size.width ? CGSize(width: size.width, height: aspectHeight) : CGSize(width: aspectWidth, height: size.height)
  281. }
  282. func kf_sizeFillingSize(size: CGSize) -> CGSize {
  283. let aspectWidth = round(kf_aspectRatio * size.height)
  284. let aspectHeight = round(size.width / kf_aspectRatio)
  285. return aspectWidth < size.width ? CGSize(width: size.width, height: aspectHeight) : CGSize(width: aspectWidth, height: size.height)
  286. }
  287. private var kf_aspectRatio: CGFloat {
  288. return height == 0.0 ? 1.0 : width / height
  289. }
  290. }
  291. extension CGImageSourceRef {
  292. func kf_GIFPropertiesAtIndex(index: Int) -> [String: Double]? {
  293. let properties = CGImageSourceCopyPropertiesAtIndex(self, index, nil) as Dictionary?
  294. return properties?[kCGImagePropertyGIFDictionary as String] as? [String: Double]
  295. }
  296. }
  297. extension Array {
  298. subscript(safe index: Int) -> Element? {
  299. return indices ~= index ? self[index] : .None
  300. }
  301. }
  302. func pure<T>(a: T) -> [T] {
  303. return [a]
  304. }