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

//
// 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) 2019 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
/// Protocol of `AnimatedImageView`.
public protocol AnimatedImageViewDelegate: AnyObject {
/// Called after the animatedImageView has finished each animation loop.
///
/// - Parameters:
/// - imageView: The `AnimatedImageView` that is being animated.
/// - count: The looped count.
func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt)
/// Called after the `AnimatedImageView` has reached the max repeat count.
///
/// - Parameter imageView: The `AnimatedImageView` that is being animated.
func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView)
}
extension AnimatedImageViewDelegate {
public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {}
public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {}
}
#if swift(>=4.2)
let KFRunLoopModeCommon = RunLoop.Mode.common
#else
let KFRunLoopModeCommon = RunLoopMode.commonModes
#endif
/// Represents a subclass of `UIImageView` for displaying animated image.
/// Different from showing animated image in a normal `UIImageView` (which load all frames at one time),
/// `AnimatedImageView` only tries to load several frames (defined by `framePreloadCount`) to reduce memory usage.
/// It provides a tradeoff between memory usage and CPU time. If you have a memory issue when using a normal image
/// view to load GIF data, you could give this class a try.
///
/// Kingfisher supports setting GIF animated data to either `UIImageView` and `AnimatedImageView` out of box. So
/// it would be fairly easy to switch between them.
open class AnimatedImageView: UIImageView {
/// Proxy object for preventing 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?.updateFrameIfNeeded()
}
}
/// Enumeration that specifies repeat count of GIF
public enum RepeatCount: Equatable {
case once
case finite(count: UInt)
case infinite
public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool {
switch (lhs, rhs) {
case let (.finite(l), .finite(r)):
return l == r
case (.once, .once),
(.infinite, .infinite):
return true
case (.once, .finite(let count)),
(.finite(let count), .once):
return count == 1
case (.once, _),
(.infinite, _),
(.finite, _):
return false
}
}
}
// MARK: - Public property
/// Whether automatically play the animation when the view become visible. Default is `true`.
public var autoPlayAnimatedImage = true
/// The count of the frames should be preloaded before shown.
public var framePreloadCount = 10
/// Specifies whether the GIF frames should be pre-scaled to the image view's size or not.
/// If the downloaded image is larger than the image view's size, it will help to reduce some memory use.
/// Default is `true`.
public var needsPrescaling = true
/// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen
/// rendering to extract pixel information in background. This can reduce the main thread CPU usage.
public var backgroundDecode = true
/// The animation timer's run loop mode. Default is `RunLoop.Mode.common`.
/// Set this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling.
public var runLoopMode = KFRunLoopModeCommon {
willSet {
guard runLoopMode == newValue else { return }
stopAnimating()
displayLink.remove(from: .main, forMode: runLoopMode)
displayLink.add(to: .main, forMode: newValue)
startAnimating()
}
}
/// The repeat count. The animated image will keep animate until it the loop count reaches this value.
/// Setting this value to another one will reset current animation.
///
/// Default is `.infinite`, which means the animation will last forever.
public var repeatCount = RepeatCount.infinite {
didSet {
if oldValue != repeatCount {
reset()
setNeedsDisplay()
layer.setNeedsDisplay()
}
}
}
/// Delegate of this `AnimatedImageView` object. See `AnimatedImageViewDelegate` protocol for more.
public weak var delegate: AnimatedImageViewDelegate?
// MARK: - Private property
/// `Animator` instance that holds the frames of a specific image in memory.
private var animator: Animator?
// Dispatch queue used for preloading images.
private lazy var preloadQueue: DispatchQueue = {
return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
}()
// A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy.
private var isDisplayLinkInitialized: Bool = false
// A display link that keeps calling the `updateFrame` method on every screen refresh.
private lazy var displayLink: CADisplayLink = {
isDisplayLinkInitialized = true
let displayLink = CADisplayLink(
target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
displayLink.add(to: .main, forMode: runLoopMode)
displayLink.isPaused = true
return displayLink
}()
// MARK: - Override
override open var image: Image? {
didSet {
if image != oldValue {
reset()
}
setNeedsDisplay()
layer.setNeedsDisplay()
}
}
deinit {
if isDisplayLinkInitialized {
displayLink.invalidate()
}
}
override open var isAnimating: Bool {
if isDisplayLinkInitialized {
return !displayLink.isPaused
} else {
return super.isAnimating
}
}
/// Starts the animation.
override open func startAnimating() {
guard !isAnimating else { return }
if animator?.isReachMaxRepeatCount ?? false {
return
}
displayLink.isPaused = false
}
/// Stops the animation.
override open func stopAnimating() {
super.stopAnimating()
if isDisplayLinkInitialized {
displayLink.isPaused = true
}
}
override open func display(_ layer: CALayer) {
if let currentFrame = animator?.currentFrameImage {
layer.contents = currentFrame.cgImage
} else {
layer.contents = image?.cgImage
}
}
override open func didMoveToWindow() {
super.didMoveToWindow()
didMove()
}
override open func didMoveToSuperview() {
super.didMoveToSuperview()
didMove()
}
// This is for back compatibility that using regular `UIImageView` to show animated image.
override func shouldPreloadAllAnimation() -> Bool {
return false
}
// Reset the animator.
private func reset() {
animator = nil
if let imageSource = image?.kf.imageSource {
let targetSize = bounds.scaled(UIScreen.main.scale).size
let animator = Animator(
imageSource: imageSource,
contentMode: contentMode,
size: targetSize,
framePreloadCount: framePreloadCount,
repeatCount: repeatCount,
preloadQueue: preloadQueue)
animator.delegate = self
animator.needsPrescaling = needsPrescaling
animator.backgroundDecode = backgroundDecode
animator.prepareFramesAsynchronously()
self.animator = animator
}
didMove()
}
private func didMove() {
if autoPlayAnimatedImage && animator != nil {
if let _ = superview, let _ = window {
startAnimating()
} else {
stopAnimating()
}
}
}
/// Update the current frame with the displayLink duration.
private func updateFrameIfNeeded() {
guard let animator = animator else {
return
}
guard !animator.isFinished else {
stopAnimating()
delegate?.animatedImageViewDidFinishAnimating(self)
return
}
let duration: CFTimeInterval
// CA based display link is opt-out from ProMotion by default.
// So the duration and its FPS might not match.
// See [#718](https://github.com/onevcat/Kingfisher/issues/718)
// By setting CADisableMinimumFrameDuration to YES in Info.plist may
// cause the preferredFramesPerSecond being 0
if displayLink.preferredFramesPerSecond == 0 {
duration = displayLink.duration
} else {
// Some devices (like iPad Pro 10.5) will have a different FPS.
duration = 1.0 / Double(displayLink.preferredFramesPerSecond)
}
animator.shouldChangeFrame(with: duration) { [weak self] hasNewFrame in
if hasNewFrame {
self?.layer.setNeedsDisplay()
}
}
}
}
protocol AnimatorDelegate: AnyObject {
func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt)
}
extension AnimatedImageView: AnimatorDelegate {
func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) {
delegate?.animatedImageView(self, didPlayAnimationLoops: count)
}
}
extension AnimatedImageView {
// Represents a single frame in a GIF.
struct AnimatedFrame {
// The image to display for this frame. Its value is nil when the frame is removed from the buffer.
let image: UIImage?
// The duration that this frame should remain active.
let duration: TimeInterval
// A placeholder frame with no image assigned.
// Used to replace frames that are no longer needed in the animation.
var placeholderFrame: AnimatedFrame {
return AnimatedFrame(image: nil, duration: duration)
}
// Whether this frame instance contains an image or not.
var isPlaceholder: Bool {
return image == nil
}
// Returns a new instance from an optional image.
//
// - parameter image: An optional `UIImage` instance to be assigned to the new frame.
// - returns: An `AnimatedFrame` instance.
func makeAnimatedFrame(image: UIImage?) -> AnimatedFrame {
return AnimatedFrame(image: image, duration: duration)
}
}
}
extension AnimatedImageView {
// MARK: - Animator
class Animator {
private let size: CGSize
private let maxFrameCount: Int
private let imageSource: CGImageSource
private let maxRepeatCount: RepeatCount
private let maxTimeStep: TimeInterval = 1.0
private var animatedFrames = [AnimatedFrame]()
private var frameCount = 0
private var timeSinceLastFrameChange: TimeInterval = 0.0
private var currentRepeatCount: UInt = 0
var isFinished: Bool = false
var needsPrescaling = true
var backgroundDecode = true
weak var delegate: AnimatorDelegate?
// Total duration of one animation loop
var loopDuration: TimeInterval = 0
// Current active frame image
var currentFrameImage: UIImage? {
return frame(at: currentFrameIndex)
}
// Current active frame duration
var currentFrameDuration: TimeInterval {
return duration(at: currentFrameIndex)
}
// The index of the current GIF frame.
var currentFrameIndex = 0 {
didSet {
previousFrameIndex = oldValue
}
}
var previousFrameIndex = 0 {
didSet {
preloadQueue.async {
self.updatePreloadedFrames()
}
}
}
var isReachMaxRepeatCount: Bool {
switch maxRepeatCount {
case .once:
return currentRepeatCount >= 1
case .finite(let maxCount):
return currentRepeatCount >= maxCount
case .infinite:
return false
}
}
var isLastFrame: Bool {
return currentFrameIndex == frameCount - 1
}
var preloadingIsNeeded: Bool {
return maxFrameCount < frameCount - 1
}
var contentMode = UIView.ContentMode.scaleToFill
private lazy var preloadQueue: DispatchQueue = {
return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
}()
/// Creates an animator with image source reference.
///
/// - Parameters:
/// - source: The reference of animated image.
/// - mode: Content mode of the `AnimatedImageView`.
/// - size: Size of the `AnimatedImageView`.
/// - count: Count of frames needed to be preloaded.
/// - repeatCount: The repeat count should this animator uses.
init(imageSource source: CGImageSource,
contentMode mode: UIView.ContentMode,
size: CGSize,
framePreloadCount count: Int,
repeatCount: RepeatCount,
preloadQueue: DispatchQueue) {
self.imageSource = source
self.contentMode = mode
self.size = size
self.maxFrameCount = count
self.maxRepeatCount = repeatCount
self.preloadQueue = preloadQueue
}
func frame(at index: Int) -> Image? {
return animatedFrames[safe: index]?.image
}
func duration(at index: Int) -> TimeInterval {
return animatedFrames[safe: index]?.duration ?? .infinity
}
func prepareFramesAsynchronously() {
frameCount = Int(CGImageSourceGetCount(imageSource))
animatedFrames.reserveCapacity(frameCount)
preloadQueue.async { [weak self] in
self?.setupAnimatedFrames()
}
}
func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) {
incrementTimeSinceLastFrameChange(with: duration)
if currentFrameDuration > timeSinceLastFrameChange {
handler(false)
} else {
resetTimeSinceLastFrameChange()
incrementCurrentFrameIndex()
handler(true)
}
}
private func setupAnimatedFrames() {
resetAnimatedFrames()
var duration: TimeInterval = 0
(0..<frameCount).forEach { index in
let frameDuration = GIFAnimatedImage.getFrameDuration(from: imageSource, at: index)
duration += min(frameDuration, maxTimeStep)
animatedFrames += [AnimatedFrame(image: nil, duration: frameDuration)]
if index > maxFrameCount { return }
animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(image: loadFrame(at: index))
}
self.loopDuration = duration
}
private func resetAnimatedFrames() {
animatedFrames = []
}
private func loadFrame(at index: Int) -> UIImage? {
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
]
let resize = needsPrescaling && size != .zero
guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource,
index,
resize ? options as CFDictionary : nil) else {
return nil
}
let image = Image(cgImage: cgImage)
return backgroundDecode ? image.kf.decoded : image
}
private func updatePreloadedFrames() {
guard preloadingIsNeeded else {
return
}
animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex].placeholderFrame
preloadIndexes(start: currentFrameIndex).forEach { index in
let currentAnimatedFrame = animatedFrames[index]
if !currentAnimatedFrame.isPlaceholder { return }
animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index))
}
}
private func incrementCurrentFrameIndex() {
currentFrameIndex = increment(frameIndex: currentFrameIndex)
if isReachMaxRepeatCount && isLastFrame {
isFinished = true
} else if currentFrameIndex == 0 {
currentRepeatCount += 1
delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)
}
}
private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) {
timeSinceLastFrameChange += min(maxTimeStep, duration)
}
private func resetTimeSinceLastFrameChange() {
timeSinceLastFrameChange -= currentFrameDuration
}
private func increment(frameIndex: Int, by value: Int = 1) -> Int {
return (frameIndex + value) % frameCount
}
private func preloadIndexes(start index: Int) -> [Int] {
let nextIndex = increment(frameIndex: index)
let lastIndex = increment(frameIndex: index, by: maxFrameCount)
if lastIndex >= nextIndex {
return [Int](nextIndex...lastIndex)
} else {
return [Int](nextIndex..<frameCount) + [Int](0...lastIndex)
}
}
}
}
extension Array {
subscript(safe index: Int) -> Element? {
return indices ~= index ? self[index] : nil
}
}