You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

570 lines
20 KiB

  1. //
  2. // AnimatableImageView.swift
  3. // Kingfisher
  4. //
  5. // Created by bl4ckra1sond3tre on 4/22/16.
  6. //
  7. // The AnimatableImageView, AnimatedFrame and Animator is a modified version of
  8. // some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu)
  9. //
  10. // The MIT License (MIT)
  11. //
  12. // Copyright (c) 2019 Reda Lemeden.
  13. //
  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:
  20. //
  21. // The above copyright notice and this permission notice shall be included in all
  22. // copies or substantial portions of the Software.
  23. //
  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.
  30. //
  31. // The name and characters used in the demo of this software are property of their
  32. // respective owners.
  33. import UIKit
  34. import ImageIO
  35. /// Protocol of `AnimatedImageView`.
  36. public protocol AnimatedImageViewDelegate: AnyObject {
  37. /// Called after the animatedImageView has finished each animation loop.
  38. ///
  39. /// - Parameters:
  40. /// - imageView: The `AnimatedImageView` that is being animated.
  41. /// - count: The looped count.
  42. func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt)
  43. /// Called after the `AnimatedImageView` has reached the max repeat count.
  44. ///
  45. /// - Parameter imageView: The `AnimatedImageView` that is being animated.
  46. func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView)
  47. }
  48. extension AnimatedImageViewDelegate {
  49. public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {}
  50. public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {}
  51. }
  52. #if swift(>=4.2)
  53. let KFRunLoopModeCommon = RunLoop.Mode.common
  54. #else
  55. let KFRunLoopModeCommon = RunLoopMode.commonModes
  56. #endif
  57. /// Represents a subclass of `UIImageView` for displaying animated image.
  58. /// Different from showing animated image in a normal `UIImageView` (which load all frames at one time),
  59. /// `AnimatedImageView` only tries to load several frames (defined by `framePreloadCount`) to reduce memory usage.
  60. /// It provides a tradeoff between memory usage and CPU time. If you have a memory issue when using a normal image
  61. /// view to load GIF data, you could give this class a try.
  62. ///
  63. /// Kingfisher supports setting GIF animated data to either `UIImageView` and `AnimatedImageView` out of box. So
  64. /// it would be fairly easy to switch between them.
  65. open class AnimatedImageView: UIImageView {
  66. /// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`.
  67. class TargetProxy {
  68. private weak var target: AnimatedImageView?
  69. init(target: AnimatedImageView) {
  70. self.target = target
  71. }
  72. @objc func onScreenUpdate() {
  73. target?.updateFrameIfNeeded()
  74. }
  75. }
  76. /// Enumeration that specifies repeat count of GIF
  77. public enum RepeatCount: Equatable {
  78. case once
  79. case finite(count: UInt)
  80. case infinite
  81. public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool {
  82. switch (lhs, rhs) {
  83. case let (.finite(l), .finite(r)):
  84. return l == r
  85. case (.once, .once),
  86. (.infinite, .infinite):
  87. return true
  88. case (.once, .finite(let count)),
  89. (.finite(let count), .once):
  90. return count == 1
  91. case (.once, _),
  92. (.infinite, _),
  93. (.finite, _):
  94. return false
  95. }
  96. }
  97. }
  98. // MARK: - Public property
  99. /// Whether automatically play the animation when the view become visible. Default is `true`.
  100. public var autoPlayAnimatedImage = true
  101. /// The count of the frames should be preloaded before shown.
  102. public var framePreloadCount = 10
  103. /// Specifies whether the GIF frames should be pre-scaled to the image view's size or not.
  104. /// If the downloaded image is larger than the image view's size, it will help to reduce some memory use.
  105. /// Default is `true`.
  106. public var needsPrescaling = true
  107. /// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen
  108. /// rendering to extract pixel information in background. This can reduce the main thread CPU usage.
  109. public var backgroundDecode = true
  110. /// The animation timer's run loop mode. Default is `RunLoop.Mode.common`.
  111. /// Set this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling.
  112. public var runLoopMode = KFRunLoopModeCommon {
  113. willSet {
  114. guard runLoopMode == newValue else { return }
  115. stopAnimating()
  116. displayLink.remove(from: .main, forMode: runLoopMode)
  117. displayLink.add(to: .main, forMode: newValue)
  118. startAnimating()
  119. }
  120. }
  121. /// The repeat count. The animated image will keep animate until it the loop count reaches this value.
  122. /// Setting this value to another one will reset current animation.
  123. ///
  124. /// Default is `.infinite`, which means the animation will last forever.
  125. public var repeatCount = RepeatCount.infinite {
  126. didSet {
  127. if oldValue != repeatCount {
  128. reset()
  129. setNeedsDisplay()
  130. layer.setNeedsDisplay()
  131. }
  132. }
  133. }
  134. /// Delegate of this `AnimatedImageView` object. See `AnimatedImageViewDelegate` protocol for more.
  135. public weak var delegate: AnimatedImageViewDelegate?
  136. // MARK: - Private property
  137. /// `Animator` instance that holds the frames of a specific image in memory.
  138. private var animator: Animator?
  139. // Dispatch queue used for preloading images.
  140. private lazy var preloadQueue: DispatchQueue = {
  141. return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
  142. }()
  143. // A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy.
  144. private var isDisplayLinkInitialized: Bool = false
  145. // A display link that keeps calling the `updateFrame` method on every screen refresh.
  146. private lazy var displayLink: CADisplayLink = {
  147. isDisplayLinkInitialized = true
  148. let displayLink = CADisplayLink(
  149. target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
  150. displayLink.add(to: .main, forMode: runLoopMode)
  151. displayLink.isPaused = true
  152. return displayLink
  153. }()
  154. // MARK: - Override
  155. override open var image: Image? {
  156. didSet {
  157. if image != oldValue {
  158. reset()
  159. }
  160. setNeedsDisplay()
  161. layer.setNeedsDisplay()
  162. }
  163. }
  164. deinit {
  165. if isDisplayLinkInitialized {
  166. displayLink.invalidate()
  167. }
  168. }
  169. override open var isAnimating: Bool {
  170. if isDisplayLinkInitialized {
  171. return !displayLink.isPaused
  172. } else {
  173. return super.isAnimating
  174. }
  175. }
  176. /// Starts the animation.
  177. override open func startAnimating() {
  178. guard !isAnimating else { return }
  179. if animator?.isReachMaxRepeatCount ?? false {
  180. return
  181. }
  182. displayLink.isPaused = false
  183. }
  184. /// Stops the animation.
  185. override open func stopAnimating() {
  186. super.stopAnimating()
  187. if isDisplayLinkInitialized {
  188. displayLink.isPaused = true
  189. }
  190. }
  191. override open func display(_ layer: CALayer) {
  192. if let currentFrame = animator?.currentFrameImage {
  193. layer.contents = currentFrame.cgImage
  194. } else {
  195. layer.contents = image?.cgImage
  196. }
  197. }
  198. override open func didMoveToWindow() {
  199. super.didMoveToWindow()
  200. didMove()
  201. }
  202. override open func didMoveToSuperview() {
  203. super.didMoveToSuperview()
  204. didMove()
  205. }
  206. // This is for back compatibility that using regular `UIImageView` to show animated image.
  207. override func shouldPreloadAllAnimation() -> Bool {
  208. return false
  209. }
  210. // Reset the animator.
  211. private func reset() {
  212. animator = nil
  213. if let imageSource = image?.kf.imageSource {
  214. let targetSize = bounds.scaled(UIScreen.main.scale).size
  215. let animator = Animator(
  216. imageSource: imageSource,
  217. contentMode: contentMode,
  218. size: targetSize,
  219. framePreloadCount: framePreloadCount,
  220. repeatCount: repeatCount,
  221. preloadQueue: preloadQueue)
  222. animator.delegate = self
  223. animator.needsPrescaling = needsPrescaling
  224. animator.backgroundDecode = backgroundDecode
  225. animator.prepareFramesAsynchronously()
  226. self.animator = animator
  227. }
  228. didMove()
  229. }
  230. private func didMove() {
  231. if autoPlayAnimatedImage && animator != nil {
  232. if let _ = superview, let _ = window {
  233. startAnimating()
  234. } else {
  235. stopAnimating()
  236. }
  237. }
  238. }
  239. /// Update the current frame with the displayLink duration.
  240. private func updateFrameIfNeeded() {
  241. guard let animator = animator else {
  242. return
  243. }
  244. guard !animator.isFinished else {
  245. stopAnimating()
  246. delegate?.animatedImageViewDidFinishAnimating(self)
  247. return
  248. }
  249. let duration: CFTimeInterval
  250. // CA based display link is opt-out from ProMotion by default.
  251. // So the duration and its FPS might not match.
  252. // See [#718](https://github.com/onevcat/Kingfisher/issues/718)
  253. // By setting CADisableMinimumFrameDuration to YES in Info.plist may
  254. // cause the preferredFramesPerSecond being 0
  255. if displayLink.preferredFramesPerSecond == 0 {
  256. duration = displayLink.duration
  257. } else {
  258. // Some devices (like iPad Pro 10.5) will have a different FPS.
  259. duration = 1.0 / Double(displayLink.preferredFramesPerSecond)
  260. }
  261. animator.shouldChangeFrame(with: duration) { [weak self] hasNewFrame in
  262. if hasNewFrame {
  263. self?.layer.setNeedsDisplay()
  264. }
  265. }
  266. }
  267. }
  268. protocol AnimatorDelegate: AnyObject {
  269. func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt)
  270. }
  271. extension AnimatedImageView: AnimatorDelegate {
  272. func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) {
  273. delegate?.animatedImageView(self, didPlayAnimationLoops: count)
  274. }
  275. }
  276. extension AnimatedImageView {
  277. // Represents a single frame in a GIF.
  278. struct AnimatedFrame {
  279. // The image to display for this frame. Its value is nil when the frame is removed from the buffer.
  280. let image: UIImage?
  281. // The duration that this frame should remain active.
  282. let duration: TimeInterval
  283. // A placeholder frame with no image assigned.
  284. // Used to replace frames that are no longer needed in the animation.
  285. var placeholderFrame: AnimatedFrame {
  286. return AnimatedFrame(image: nil, duration: duration)
  287. }
  288. // Whether this frame instance contains an image or not.
  289. var isPlaceholder: Bool {
  290. return image == nil
  291. }
  292. // Returns a new instance from an optional image.
  293. //
  294. // - parameter image: An optional `UIImage` instance to be assigned to the new frame.
  295. // - returns: An `AnimatedFrame` instance.
  296. func makeAnimatedFrame(image: UIImage?) -> AnimatedFrame {
  297. return AnimatedFrame(image: image, duration: duration)
  298. }
  299. }
  300. }
  301. extension AnimatedImageView {
  302. // MARK: - Animator
  303. class Animator {
  304. private let size: CGSize
  305. private let maxFrameCount: Int
  306. private let imageSource: CGImageSource
  307. private let maxRepeatCount: RepeatCount
  308. private let maxTimeStep: TimeInterval = 1.0
  309. private var animatedFrames = [AnimatedFrame]()
  310. private var frameCount = 0
  311. private var timeSinceLastFrameChange: TimeInterval = 0.0
  312. private var currentRepeatCount: UInt = 0
  313. var isFinished: Bool = false
  314. var needsPrescaling = true
  315. var backgroundDecode = true
  316. weak var delegate: AnimatorDelegate?
  317. // Total duration of one animation loop
  318. var loopDuration: TimeInterval = 0
  319. // Current active frame image
  320. var currentFrameImage: UIImage? {
  321. return frame(at: currentFrameIndex)
  322. }
  323. // Current active frame duration
  324. var currentFrameDuration: TimeInterval {
  325. return duration(at: currentFrameIndex)
  326. }
  327. // The index of the current GIF frame.
  328. var currentFrameIndex = 0 {
  329. didSet {
  330. previousFrameIndex = oldValue
  331. }
  332. }
  333. var previousFrameIndex = 0 {
  334. didSet {
  335. preloadQueue.async {
  336. self.updatePreloadedFrames()
  337. }
  338. }
  339. }
  340. var isReachMaxRepeatCount: Bool {
  341. switch maxRepeatCount {
  342. case .once:
  343. return currentRepeatCount >= 1
  344. case .finite(let maxCount):
  345. return currentRepeatCount >= maxCount
  346. case .infinite:
  347. return false
  348. }
  349. }
  350. var isLastFrame: Bool {
  351. return currentFrameIndex == frameCount - 1
  352. }
  353. var preloadingIsNeeded: Bool {
  354. return maxFrameCount < frameCount - 1
  355. }
  356. var contentMode = UIView.ContentMode.scaleToFill
  357. private lazy var preloadQueue: DispatchQueue = {
  358. return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
  359. }()
  360. /// Creates an animator with image source reference.
  361. ///
  362. /// - Parameters:
  363. /// - source: The reference of animated image.
  364. /// - mode: Content mode of the `AnimatedImageView`.
  365. /// - size: Size of the `AnimatedImageView`.
  366. /// - count: Count of frames needed to be preloaded.
  367. /// - repeatCount: The repeat count should this animator uses.
  368. init(imageSource source: CGImageSource,
  369. contentMode mode: UIView.ContentMode,
  370. size: CGSize,
  371. framePreloadCount count: Int,
  372. repeatCount: RepeatCount,
  373. preloadQueue: DispatchQueue) {
  374. self.imageSource = source
  375. self.contentMode = mode
  376. self.size = size
  377. self.maxFrameCount = count
  378. self.maxRepeatCount = repeatCount
  379. self.preloadQueue = preloadQueue
  380. }
  381. func frame(at index: Int) -> Image? {
  382. return animatedFrames[safe: index]?.image
  383. }
  384. func duration(at index: Int) -> TimeInterval {
  385. return animatedFrames[safe: index]?.duration ?? .infinity
  386. }
  387. func prepareFramesAsynchronously() {
  388. frameCount = Int(CGImageSourceGetCount(imageSource))
  389. animatedFrames.reserveCapacity(frameCount)
  390. preloadQueue.async { [weak self] in
  391. self?.setupAnimatedFrames()
  392. }
  393. }
  394. func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) {
  395. incrementTimeSinceLastFrameChange(with: duration)
  396. if currentFrameDuration > timeSinceLastFrameChange {
  397. handler(false)
  398. } else {
  399. resetTimeSinceLastFrameChange()
  400. incrementCurrentFrameIndex()
  401. handler(true)
  402. }
  403. }
  404. private func setupAnimatedFrames() {
  405. resetAnimatedFrames()
  406. var duration: TimeInterval = 0
  407. (0..<frameCount).forEach { index in
  408. let frameDuration = GIFAnimatedImage.getFrameDuration(from: imageSource, at: index)
  409. duration += min(frameDuration, maxTimeStep)
  410. animatedFrames += [AnimatedFrame(image: nil, duration: frameDuration)]
  411. if index > maxFrameCount { return }
  412. animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(image: loadFrame(at: index))
  413. }
  414. self.loopDuration = duration
  415. }
  416. private func resetAnimatedFrames() {
  417. animatedFrames = []
  418. }
  419. private func loadFrame(at index: Int) -> UIImage? {
  420. let options: [CFString: Any] = [
  421. kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
  422. kCGImageSourceCreateThumbnailWithTransform: true,
  423. kCGImageSourceShouldCacheImmediately: true,
  424. kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
  425. ]
  426. let resize = needsPrescaling && size != .zero
  427. guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource,
  428. index,
  429. resize ? options as CFDictionary : nil) else {
  430. return nil
  431. }
  432. let image = Image(cgImage: cgImage)
  433. return backgroundDecode ? image.kf.decoded : image
  434. }
  435. private func updatePreloadedFrames() {
  436. guard preloadingIsNeeded else {
  437. return
  438. }
  439. animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex].placeholderFrame
  440. preloadIndexes(start: currentFrameIndex).forEach { index in
  441. let currentAnimatedFrame = animatedFrames[index]
  442. if !currentAnimatedFrame.isPlaceholder { return }
  443. animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index))
  444. }
  445. }
  446. private func incrementCurrentFrameIndex() {
  447. currentFrameIndex = increment(frameIndex: currentFrameIndex)
  448. if isReachMaxRepeatCount && isLastFrame {
  449. isFinished = true
  450. } else if currentFrameIndex == 0 {
  451. currentRepeatCount += 1
  452. delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)
  453. }
  454. }
  455. private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) {
  456. timeSinceLastFrameChange += min(maxTimeStep, duration)
  457. }
  458. private func resetTimeSinceLastFrameChange() {
  459. timeSinceLastFrameChange -= currentFrameDuration
  460. }
  461. private func increment(frameIndex: Int, by value: Int = 1) -> Int {
  462. return (frameIndex + value) % frameCount
  463. }
  464. private func preloadIndexes(start index: Int) -> [Int] {
  465. let nextIndex = increment(frameIndex: index)
  466. let lastIndex = increment(frameIndex: index, by: maxFrameCount)
  467. if lastIndex >= nextIndex {
  468. return [Int](nextIndex...lastIndex)
  469. } else {
  470. return [Int](nextIndex..<frameCount) + [Int](0...lastIndex)
  471. }
  472. }
  473. }
  474. }
  475. extension Array {
  476. subscript(safe index: Int) -> Element? {
  477. return indices ~= index ? self[index] : nil
  478. }
  479. }