added iOS source code
[wl-app.git] / iOS / Pods / Kingfisher / Sources / AnimatedImageView.swift
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) 2018 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
34 import UIKit
35 import ImageIO
36
37 /// Protocol of `AnimatedImageView`.
38 public protocol AnimatedImageViewDelegate: class {
39     /**
40      Called after the animatedImageView has finished each animation loop.
41
42      - parameter imageView: The animatedImageView that is being animated.
43      - parameter count: The looped count.
44      */
45     func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt)
46
47     /**
48      Called after the animatedImageView has reached the max repeat count.
49
50      - parameter imageView: The animatedImageView that is being animated.
51      */
52     func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView)
53 }
54
55 extension AnimatedImageViewDelegate {
56     public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {}
57     public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {}
58 }
59
60 /// `AnimatedImageView` is a subclass of `UIImageView` for displaying animated image.
61 open class AnimatedImageView: UIImageView {
62     
63     /// Proxy object for prevending a reference cycle between the CADDisplayLink and AnimatedImageView.
64     class TargetProxy {
65         private weak var target: AnimatedImageView?
66         
67         init(target: AnimatedImageView) {
68             self.target = target
69         }
70         
71         @objc func onScreenUpdate() {
72             target?.updateFrame()
73         }
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
82         public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool {
83             switch (lhs, rhs) {
84             case let (.finite(l), .finite(r)):
85                 return l == r
86             case (.once, .once),
87                  (.infinite, .infinite):
88                 return true
89             case (.once, _),
90                  (.infinite, _),
91                  (.finite, _):
92                 return false
93             }
94         }
95     }
96     
97     // MARK: - Public property
98     /// Whether automatically play the animation when the view become visible. Default is true.
99     public var autoPlayAnimatedImage = true
100     
101     /// The size of the frame cache.
102     public var framePreloadCount = 10
103     
104     /// Specifies whether the GIF frames should be pre-scaled to save memory. Default is true.
105     public var needsPrescaling = true
106     
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 {
109         willSet {
110             if runLoopMode == newValue {
111                 return
112             } else {
113                 stopAnimating()
114                 displayLink.remove(from: .main, forMode: runLoopMode)
115                 displayLink.add(to: .main, forMode: newValue)
116                 startAnimating()
117             }
118         }
119     }
120
121     /// The repeat count.
122     public var repeatCount = RepeatCount.infinite {
123         didSet {
124             if oldValue != repeatCount {
125                 reset()
126                 setNeedsDisplay()
127                 layer.setNeedsDisplay()
128             }
129         }
130     }
131
132     /// Delegate of this `AnimatedImageView` object. See `AnimatedImageViewDelegate` protocol for more.
133     public weak var delegate: AnimatedImageViewDelegate?
134     
135     // MARK: - Private property
136     /// `Animator` instance that holds the frames of a specific image in memory.
137     private var animator: Animator?
138     
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
141     
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
148         return displayLink
149     }()
150     
151     // MARK: - Override
152     override open var image: Image? {
153         didSet {
154             if image != oldValue {
155                 reset()
156             }
157             setNeedsDisplay()
158             layer.setNeedsDisplay()
159         }
160     }
161     
162     deinit {
163         if isDisplayLinkInitialized {
164             displayLink.invalidate()
165         }
166     }
167     
168     override open var isAnimating: Bool {
169         if isDisplayLinkInitialized {
170             return !displayLink.isPaused
171         } else {
172             return super.isAnimating
173         }
174     }
175     
176     /// Starts the animation.
177     override open func startAnimating() {
178         if self.isAnimating {
179             return
180         } else {
181             if animator?.isReachMaxRepeatCount ?? false {
182                 return
183             }
184
185             displayLink.isPaused = false
186         }
187     }
188     
189     /// Stops the animation.
190     override open func stopAnimating() {
191         super.stopAnimating()
192         if isDisplayLinkInitialized {
193             displayLink.isPaused = true
194         }
195     }
196     
197     override open func display(_ layer: CALayer) {
198         if let currentFrame = animator?.currentFrame {
199             layer.contents = currentFrame.cgImage
200         } else {
201             layer.contents = image?.cgImage
202         }
203     }
204     
205     override open func didMoveToWindow() {
206         super.didMoveToWindow()
207         didMove()
208     }
209     
210     override open func didMoveToSuperview() {
211         super.didMoveToSuperview()
212         didMove()
213     }
214
215     // This is for back compatibility that using regular UIImageView to show animated image.
216     override func shouldPreloadAllAnimation() -> Bool {
217         return false
218     }
219
220     // MARK: - Private method
221     /// Reset the animator.
222     private func reset() {
223         animator = nil
224         if let imageSource = image?.kf.imageSource?.imageRef {
225             animator = Animator(imageSource: imageSource,
226                                 contentMode: contentMode,
227                                 size: bounds.size,
228                                 framePreloadCount: framePreloadCount,
229                                 repeatCount: repeatCount)
230             animator?.delegate = self
231             animator?.needsPrescaling = needsPrescaling
232             animator?.prepareFramesAsynchronously()
233         }
234         didMove()
235     }
236     
237     private func didMove() {
238         if autoPlayAnimatedImage && animator != nil {
239             if let _ = superview, let _ = window {
240                 startAnimating()
241             } else {
242                 stopAnimating()
243             }
244         }
245     }
246     
247     /// Update the current frame with the displayLink duration.
248     private func updateFrame() {
249         let duration: CFTimeInterval
250
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
259             } else {
260                 // Some devices (like iPad Pro 10.5) will have a different FPS.
261                 duration = 1.0 / Double(displayLink.preferredFramesPerSecond)
262             }
263         } else {
264             duration = displayLink.duration
265         }
266     
267         if animator?.updateCurrentFrame(duration: duration) ?? false {
268             layer.setNeedsDisplay()
269
270             if animator?.isReachMaxRepeatCount ?? false {
271                 stopAnimating()
272                 delegate?.animatedImageViewDidFinishAnimating(self)
273             }
274         }
275     }
276 }
277
278 extension AnimatedImageView: AnimatorDelegate {
279     func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) {
280         delegate?.animatedImageView(self, didPlayAnimationLoops: count)
281     }
282 }
283
284 /// Keeps a reference to an `Image` instance and its duration as a GIF frame.
285 struct AnimatedFrame {
286     var image: Image?
287     let duration: TimeInterval
288     
289     static let null: AnimatedFrame = AnimatedFrame(image: .none, duration: 0.0)
290 }
291
292 protocol AnimatorDelegate: class {
293     func animator(_ animator: Animator, didPlayAnimationLoops count: UInt)
294 }
295
296 // MARK: - Animator
297 class Animator {
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
303     
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?
314     
315     /// Loop count of animated image.
316     private var loopCount = 0
317     
318     var currentFrame: UIImage? {
319         return frame(at: currentFrameIndexInBuffer)
320     }
321
322     var isReachMaxRepeatCount: Bool {
323         switch maxRepeatCount {
324         case .once:
325             return currentRepeatCount >= 1
326         case .finite(let maxCount):
327             return currentRepeatCount >= maxCount
328         case .infinite:
329             return false
330         }
331     }
332     
333     var contentMode = UIViewContentMode.scaleToFill
334     
335     private lazy var preloadQueue: DispatchQueue = {
336         return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
337     }()
338     
339     /**
340      Init an animator with image source reference.
341      
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.
346      
347      - returns: The animator object.
348      */
349     init(imageSource source: CGImageSource,
350          contentMode mode: UIViewContentMode,
351          size: CGSize,
352          framePreloadCount count: Int,
353          repeatCount: AnimatedImageView.RepeatCount) {
354         self.imageSource = source
355         self.contentMode = mode
356         self.size = size
357         self.maxFrameCount = count
358         self.maxRepeatCount = repeatCount
359     }
360     
361     func frame(at index: Int) -> Image? {
362         return animatedFrames[safe: index]?.image
363     }
364     
365     func prepareFramesAsynchronously() {
366         preloadQueue.async { [weak self] in
367             self?.prepareFrames()
368         }
369     }
370     
371     private func prepareFrames() {
372         frameCount = CGImageSourceGetCount(imageSource)
373         
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
377         {
378             self.loopCount = loopCount
379         }
380         
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
385     }
386     
387     private func prepareFrame(at index: Int) -> AnimatedFrame {
388         
389         guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else {
390             return AnimatedFrame.null
391         }
392         
393         let defaultGIFFrameDuration = 0.100
394         let frameDuration = imageSource.kf.gifProperties(at: index).map {
395             gifInfo -> Double in
396             
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
400             
401             /**
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.
407              
408              See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser.
409              */
410             return duration > 0.011 ? duration : defaultGIFFrameDuration
411         } ?? defaultGIFFrameDuration
412         
413         let image = Image(cgImage: imageRef)
414         let scaledImage: Image?
415         
416         if needsPrescaling {
417             scaledImage = image.kf.resize(to: size, for: contentMode)
418         } else {
419             scaledImage = image
420         }
421         
422         return AnimatedFrame(image: scaledImage, duration: frameDuration)
423     }
424     
425     /**
426      Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`.
427      */
428     func updateCurrentFrame(duration: CFTimeInterval) -> Bool {
429         timeSinceLastFrameChange += min(maxTimeStep, duration)
430         guard let frameDuration = animatedFrames[safe: currentFrameIndexInBuffer]?.duration, frameDuration <= timeSinceLastFrameChange else {
431             return false
432         }
433         
434         timeSinceLastFrameChange -= frameDuration
435         
436         let lastFrameIndex = currentFrameIndexInBuffer
437         currentFrameIndexInBuffer += 1
438         currentFrameIndexInBuffer = currentFrameIndexInBuffer % animatedFrames.count
439         
440         if animatedFrames.count < frameCount {
441             preloadFrameAsynchronously(at: lastFrameIndex)
442         }
443         
444         currentFrameIndex += 1
445         
446         if currentFrameIndex == frameCount {
447             currentFrameIndex = 0
448             currentRepeatCount += 1
449
450             delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)
451         }
452
453         return true
454     }
455     
456     private func preloadFrameAsynchronously(at index: Int) {
457         preloadQueue.async { [weak self] in
458             self?.preloadFrame(at: index)
459         }
460     }
461     
462     private func preloadFrame(at index: Int) {
463         animatedFrames[index] = prepareFrame(at: currentPreloadIndex)
464         currentPreloadIndex += 1
465         currentPreloadIndex = currentPreloadIndex % frameCount
466     }
467 }
468
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]
474     }
475 }
476
477 extension Array {
478     fileprivate subscript(safe index: Int) -> Element? {
479         return indices ~= index ? self[index] : nil
480     }
481 }
482
483 private func pure<T>(_ value: T) -> [T] {
484     return [value]
485 }