| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- //
- // AnimatableImageView.swift
- // Kingfisher
- //
- // Created by bl4ckra1sond3tre on 4/22/16.
- //
- // The AnimatableImageView, AnimatedFrame and Animator is a modified version of
- // some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu)
- //
- // The MIT License (MIT)
- //
- // Copyright (c) 2014-2016 Reda Lemeden.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy of
- // this software and associated documentation files (the "Software"), to deal in
- // the Software without restriction, including without limitation the rights to
- // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- // the Software, and to permit persons to whom the Software is furnished to do so,
- // subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in all
- // copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- //
- // The name and characters used in the demo of this software are property of their
- // respective owners.
- import UIKit
- import ImageIO
- /// `AnimatedImageView` is a subclass of `UIImageView` for displaying animated image.
- public class AnimatedImageView: UIImageView {
-
- /// Proxy object for prevending a reference cycle between the CADDisplayLink and AnimatedImageView.
- class TargetProxy {
- private weak var target: AnimatedImageView?
-
- init(target: AnimatedImageView) {
- self.target = target
- }
-
- @objc func onScreenUpdate() {
- target?.updateFrame()
- }
- }
-
- // MARK: - Public property
- /// Whether automatically play the animation when the view become visible. Default is true.
- public var autoPlayAnimatedImage = true
-
- /// The size of the frame cache.
- public var framePreloadCount = 10
-
- /// Specifies whether the GIF frames should be pre-scaled to save memory. Default is true.
- public var needsPrescaling = true
-
- /// The animation timer's run loop mode. Default is `NSRunLoopCommonModes`. Set this property to `NSDefaultRunLoopMode` will make the animation pause during UIScrollView scrolling.
- public var runLoopMode = NSRunLoopCommonModes {
- willSet {
- if runLoopMode == newValue {
- return
- } else {
- stopAnimating()
- displayLink.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: runLoopMode)
- displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: newValue)
- startAnimating()
- }
- }
- }
-
- // MARK: - Private property
- /// `Animator` instance that holds the frames of a specific image in memory.
- private var animator: Animator?
-
- /// A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. :D
- private var displayLinkInitialized: Bool = false
-
- /// A display link that keeps calling the `updateFrame` method on every screen refresh.
- private lazy var displayLink: CADisplayLink = {
- self.displayLinkInitialized = true
- let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
- displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: self.runLoopMode)
- displayLink.paused = true
- return displayLink
- }()
-
- // MARK: - Override
- override public var image: Image? {
- didSet {
- if image != oldValue {
- reset()
- }
- setNeedsDisplay()
- layer.setNeedsDisplay()
- }
- }
-
- deinit {
- if displayLinkInitialized {
- displayLink.invalidate()
- }
- }
-
- override public func isAnimating() -> Bool {
- if displayLinkInitialized {
- return !displayLink.paused
- } else {
- return super.isAnimating()
- }
- }
-
- /// Starts the animation.
- override public func startAnimating() {
- if self.isAnimating() {
- return
- } else {
- displayLink.paused = false
- }
- }
-
- /// Stops the animation.
- override public func stopAnimating() {
- super.stopAnimating()
- if displayLinkInitialized {
- displayLink.paused = true
- }
- }
-
- override public func displayLayer(layer: CALayer) {
- if let currentFrame = animator?.currentFrame {
- layer.contents = currentFrame.CGImage
- } else {
- layer.contents = image?.CGImage
- }
- }
-
- override public func didMoveToWindow() {
- super.didMoveToWindow()
- didMove()
- }
-
- override public func didMoveToSuperview() {
- super.didMoveToSuperview()
- didMove()
- }
-
- // This is for back compatibility that using regular UIImageView to show GIF.
- override func shouldPreloadAllGIF() -> Bool {
- return false
- }
-
- // MARK: - Private method
- /// Reset the animator.
- private func reset() {
- animator = nil
- if let imageSource = image?.kf_imageSource?.imageRef {
- animator = Animator(imageSource: imageSource, contentMode: contentMode, size: bounds.size, framePreloadCount: framePreloadCount)
- animator?.needsPrescaling = needsPrescaling
- animator?.prepareFrames()
- }
- didMove()
- }
-
- private func didMove() {
- if autoPlayAnimatedImage && animator != nil {
- if let _ = superview, _ = window {
- startAnimating()
- } else {
- stopAnimating()
- }
- }
- }
-
- /// Update the current frame with the displayLink duration.
- private func updateFrame() {
- if animator?.updateCurrentFrame(displayLink.duration) ?? false {
- layer.setNeedsDisplay()
- }
- }
- }
- /// Keeps a reference to an `Image` instance and its duration as a GIF frame.
- struct AnimatedFrame {
- var image: Image?
- let duration: NSTimeInterval
-
- static func null() -> AnimatedFrame {
- return AnimatedFrame(image: .None, duration: 0.0)
- }
- }
- // MARK: - Animator
- ///
- class Animator {
- // MARK: Private property
- private let size: CGSize
- private let maxFrameCount: Int
- private let imageSource: CGImageSourceRef
-
- private var animatedFrames = [AnimatedFrame]()
- private let maxTimeStep: NSTimeInterval = 1.0
- private var frameCount = 0
- private var currentFrameIndex = 0
- private var currentPreloadIndex = 0
- private var timeSinceLastFrameChange: NSTimeInterval = 0.0
- private var needsPrescaling = true
-
- /// Loop count of animatd image.
- private var loopCount = 0
-
- var currentFrame: UIImage? {
- return frameAtIndex(currentFrameIndex)
- }
-
- var contentMode: UIViewContentMode = .ScaleToFill
-
- /**
- Init an animator with image source reference.
-
- - parameter imageSource: The reference of animated image.
-
- - parameter contentMode: Content mode of AnimatedImageView.
-
- - parameter size: Size of AnimatedImageView.
-
- - framePreloadCount: Frame cache size.
-
- - returns: The animator object.
- */
- init(imageSource src: CGImageSourceRef, contentMode mode: UIViewContentMode, size: CGSize, framePreloadCount: Int) {
- self.imageSource = src
- self.contentMode = mode
- self.size = size
- self.maxFrameCount = framePreloadCount
- }
-
- func frameAtIndex(index: Int) -> Image? {
- return animatedFrames[index].image
- }
-
- func prepareFrames() {
- frameCount = CGImageSourceGetCount(imageSource)
-
- if let properties = CGImageSourceCopyProperties(imageSource, nil),
- gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary,
- loopCount = gifInfo[kCGImagePropertyGIFLoopCount as String] as? Int {
- self.loopCount = loopCount
- }
-
- let frameToProcess = min(frameCount, maxFrameCount)
- animatedFrames.reserveCapacity(frameToProcess)
- animatedFrames = (0..<frameToProcess).reduce([]) { $0 + pure(prepareFrame($1))}
- }
-
- func prepareFrame(index: Int) -> AnimatedFrame {
- guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else {
- return AnimatedFrame.null()
- }
-
- let frameDuration = imageSource.kf_GIFPropertiesAtIndex(index).flatMap { (gifInfo) -> Double? in
- let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as Double?
- let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as Double?
- let duration = unclampedDelayTime ?? delayTime
- /**
- http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp
- Many annoying ads specify a 0 duration to make an image flash as quickly as
- possible. We follow Safari and Firefox's behavior and use a duration of 100 ms
- for any frames that specify a duration of <= 10 ms.
- See <rdar://problem/7689300> and <http://webkit.org/b/36082> for more information.
-
- See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser.
- */
- return duration > 0.011 ? duration : 0.100
- }
-
- let image = Image(CGImage: imageRef)
- let scaledImage: Image?
-
- if needsPrescaling {
- scaledImage = image.kf_resizeToSize(size, contentMode: contentMode)
- } else {
- scaledImage = image
- }
-
- return AnimatedFrame(image: scaledImage, duration: frameDuration ?? 0.0)
- }
-
- /**
- Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`.
- */
- func updateCurrentFrame(duration: CFTimeInterval) -> Bool {
- timeSinceLastFrameChange += min(maxTimeStep, duration)
- guard let frameDuration = animatedFrames[safe: currentFrameIndex]?.duration where frameDuration <= timeSinceLastFrameChange else {
- return false
- }
-
- timeSinceLastFrameChange -= frameDuration
- let lastFrameIndex = currentFrameIndex
- currentFrameIndex += 1
- currentFrameIndex = currentFrameIndex % animatedFrames.count
-
- if animatedFrames.count < frameCount {
- animatedFrames[lastFrameIndex] = prepareFrame(currentPreloadIndex)
- currentPreloadIndex += 1
- currentPreloadIndex = currentPreloadIndex % frameCount
- }
- return true
- }
- }
- // MARK: - Resize
- extension Image {
- func kf_resizeToSize(size: CGSize, contentMode: UIViewContentMode) -> Image {
- switch contentMode {
- case .ScaleAspectFit:
- let newSize = self.size.kf_sizeConstrainedSize(size)
- return kf_resizeToSize(newSize)
- case .ScaleAspectFill:
- let newSize = self.size.kf_sizeFillingSize(size)
- return kf_resizeToSize(newSize)
- default:
- return kf_resizeToSize(size)
- }
- }
-
- private func kf_resizeToSize(size: CGSize) -> Image {
- UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
- drawInRect(CGRect(origin: CGPoint.zero, size: size))
- let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
- UIGraphicsEndImageContext()
- return resizedImage ?? self
- }
- }
- extension CGSize {
- func kf_sizeConstrainedSize(size: CGSize) -> CGSize {
- let aspectWidth = round(kf_aspectRatio * size.height)
- let aspectHeight = round(size.width / kf_aspectRatio)
-
- return aspectWidth > size.width ? CGSize(width: size.width, height: aspectHeight) : CGSize(width: aspectWidth, height: size.height)
- }
-
- func kf_sizeFillingSize(size: CGSize) -> CGSize {
- let aspectWidth = round(kf_aspectRatio * size.height)
- let aspectHeight = round(size.width / kf_aspectRatio)
-
- return aspectWidth < size.width ? CGSize(width: size.width, height: aspectHeight) : CGSize(width: aspectWidth, height: size.height)
- }
- private var kf_aspectRatio: CGFloat {
- return height == 0.0 ? 1.0 : width / height
- }
- }
- extension CGImageSourceRef {
- func kf_GIFPropertiesAtIndex(index: Int) -> [String: Double]? {
- let properties = CGImageSourceCopyPropertiesAtIndex(self, index, nil) as Dictionary?
- return properties?[kCGImagePropertyGIFDictionary as String] as? [String: Double]
- }
- }
- extension Array {
- subscript(safe index: Int) -> Element? {
- return indices ~= index ? self[index] : .None
- }
- }
- private func pure<T>(value: T) -> [T] {
- return [value]
- }
|