update
This commit is contained in:
725
Pods/Kingfisher/Sources/Views/AnimatedImageView.swift
generated
Normal file
725
Pods/Kingfisher/Sources/Views/AnimatedImageView.swift
generated
Normal file
@@ -0,0 +1,725 @@
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if !os(watchOS)
|
||||
#if canImport(UIKit)
|
||||
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) {}
|
||||
}
|
||||
|
||||
let KFRunLoopModeCommon = RunLoop.Mode.common
|
||||
|
||||
/// 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?
|
||||
|
||||
/// The `Animator` instance that holds the frames of a specific image in memory.
|
||||
public private(set) var animator: Animator?
|
||||
|
||||
// MARK: - Private property
|
||||
// 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: KFCrossPlatformImage? {
|
||||
didSet {
|
||||
if image != oldValue {
|
||||
reset()
|
||||
}
|
||||
setNeedsDisplay()
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
open override var isHighlighted: Bool {
|
||||
get {
|
||||
super.isHighlighted
|
||||
}
|
||||
set {
|
||||
// Highlighted image is unsupported for animated images.
|
||||
// See https://github.com/onevcat/Kingfisher/issues/1679
|
||||
if displayLink.isPaused {
|
||||
super.isHighlighted = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for Apple xcframework creating issue on Apple TV in Swift 5.8.
|
||||
// https://github.com/apple/swift/issues/66015
|
||||
#if os(tvOS)
|
||||
public override init(image: UIImage?, highlightedImage: UIImage?) {
|
||||
super.init(image: image, highlightedImage: highlightedImage)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
#endif
|
||||
|
||||
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 }
|
||||
guard let animator = animator else { return }
|
||||
guard !animator.isReachMaxRepeatCount else { return }
|
||||
|
||||
displayLink.isPaused = false
|
||||
}
|
||||
|
||||
/// Stops the animation.
|
||||
override open func stopAnimating() {
|
||||
super.stopAnimating()
|
||||
if isDisplayLinkInitialized {
|
||||
displayLink.isPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
override open func display(_ layer: CALayer) {
|
||||
layer.contents = animator?.currentFrameImage?.cgImage ?? 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 image = image, let frameSource = image.kf.frameSource {
|
||||
#if os(xrOS)
|
||||
let targetSize = bounds.scaled(UITraitCollection.current.displayScale).size
|
||||
#else
|
||||
let targetSize = bounds.scaled(UIScreen.main.scale).size
|
||||
#endif
|
||||
let animator = Animator(
|
||||
frameSource: frameSource,
|
||||
contentMode: contentMode,
|
||||
size: targetSize,
|
||||
imageSize: image.kf.size,
|
||||
imageScale: image.kf.scale,
|
||||
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
|
||||
let preferredFramesPerSecond = displayLink.preferredFramesPerSecond
|
||||
if preferredFramesPerSecond == 0 {
|
||||
duration = displayLink.duration
|
||||
} else {
|
||||
// Some devices (like iPad Pro 10.5) will have a different FPS.
|
||||
duration = 1.0 / TimeInterval(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
|
||||
|
||||
/// An animator which used to drive the data behind `AnimatedImageView`.
|
||||
public class Animator {
|
||||
private let size: CGSize
|
||||
|
||||
private let imageSize: CGSize
|
||||
private let imageScale: CGFloat
|
||||
|
||||
/// The maximum count of image frames that needs preload.
|
||||
public let maxFrameCount: Int
|
||||
|
||||
private let frameSource: ImageFrameSource
|
||||
private let maxRepeatCount: RepeatCount
|
||||
|
||||
private let maxTimeStep: TimeInterval = 1.0
|
||||
private let animatedFrames = SafeArray<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
|
||||
|
||||
/// The image of the current frame.
|
||||
public var currentFrameImage: UIImage? {
|
||||
return frame(at: currentFrameIndex)
|
||||
}
|
||||
|
||||
/// The duration of the current active frame duration.
|
||||
public var currentFrameDuration: TimeInterval {
|
||||
return duration(at: currentFrameIndex)
|
||||
}
|
||||
|
||||
/// The index of the current animation frame.
|
||||
public internal(set) 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the current frame is the last frame or not in the animation sequence.
|
||||
public 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`.
|
||||
/// - imageSize: Size of the `KingfisherWrapper`.
|
||||
/// - imageScale: Scale of the `KingfisherWrapper`.
|
||||
/// - count: Count of frames needed to be preloaded.
|
||||
/// - repeatCount: The repeat count should this animator uses.
|
||||
/// - preloadQueue: Dispatch queue used for preloading images.
|
||||
convenience init(imageSource source: CGImageSource,
|
||||
contentMode mode: UIView.ContentMode,
|
||||
size: CGSize,
|
||||
imageSize: CGSize,
|
||||
imageScale: CGFloat,
|
||||
framePreloadCount count: Int,
|
||||
repeatCount: RepeatCount,
|
||||
preloadQueue: DispatchQueue) {
|
||||
let frameSource = CGImageFrameSource(data: nil, imageSource: source, options: nil)
|
||||
self.init(frameSource: frameSource,
|
||||
contentMode: mode,
|
||||
size: size,
|
||||
imageSize: imageSize,
|
||||
imageScale: imageScale,
|
||||
framePreloadCount: count,
|
||||
repeatCount: repeatCount,
|
||||
preloadQueue: preloadQueue)
|
||||
}
|
||||
|
||||
/// Creates an animator with a custom image frame source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - frameSource: The reference of animated image.
|
||||
/// - mode: Content mode of the `AnimatedImageView`.
|
||||
/// - size: Size of the `AnimatedImageView`.
|
||||
/// - imageSize: Size of the `KingfisherWrapper`.
|
||||
/// - imageScale: Scale of the `KingfisherWrapper`.
|
||||
/// - count: Count of frames needed to be preloaded.
|
||||
/// - repeatCount: The repeat count should this animator uses.
|
||||
/// - preloadQueue: Dispatch queue used for preloading images.
|
||||
init(frameSource source: ImageFrameSource,
|
||||
contentMode mode: UIView.ContentMode,
|
||||
size: CGSize,
|
||||
imageSize: CGSize,
|
||||
imageScale: CGFloat,
|
||||
framePreloadCount count: Int,
|
||||
repeatCount: RepeatCount,
|
||||
preloadQueue: DispatchQueue) {
|
||||
self.frameSource = source
|
||||
self.contentMode = mode
|
||||
self.size = size
|
||||
self.imageSize = imageSize
|
||||
self.imageScale = imageScale
|
||||
self.maxFrameCount = count
|
||||
self.maxRepeatCount = repeatCount
|
||||
self.preloadQueue = preloadQueue
|
||||
|
||||
GraphicsContext.begin(size: imageSize, scale: imageScale)
|
||||
}
|
||||
|
||||
deinit {
|
||||
resetAnimatedFrames()
|
||||
GraphicsContext.end()
|
||||
}
|
||||
|
||||
/// Gets the image frame of a given index.
|
||||
/// - Parameter index: The index of desired image.
|
||||
/// - Returns: The decoded image at the frame. `nil` if the index is out of bound or the image is not yet loaded.
|
||||
public func frame(at index: Int) -> KFCrossPlatformImage? {
|
||||
return animatedFrames[index]?.image
|
||||
}
|
||||
|
||||
public func duration(at index: Int) -> TimeInterval {
|
||||
return animatedFrames[index]?.duration ?? .infinity
|
||||
}
|
||||
|
||||
func prepareFramesAsynchronously() {
|
||||
frameCount = frameSource.frameCount
|
||||
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 = frameSource.duration(at: index)
|
||||
duration += min(frameDuration, maxTimeStep)
|
||||
animatedFrames.append(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.removeAll()
|
||||
}
|
||||
|
||||
private func loadFrame(at index: Int) -> UIImage? {
|
||||
let resize = needsPrescaling && size != .zero
|
||||
let maxSize = resize ? size : nil
|
||||
guard let cgImage = frameSource.frame(at: index, maxSize: maxSize) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if #available(iOS 15, tvOS 15, *) {
|
||||
// From iOS 15, a plain image loading causes iOS calling `-[_UIImageCGImageContent initWithCGImage:scale:]`
|
||||
// in ImageIO, which holds the image ref on the creating thread.
|
||||
// To get a workaround, create another image ref and use that to create the final image. This leads to
|
||||
// some performance loss, but there is little we can do.
|
||||
// https://github.com/onevcat/Kingfisher/issues/1844
|
||||
guard let context = GraphicsContext.current(size: imageSize, scale: imageScale, inverting: true, cgImage: cgImage),
|
||||
let decodedImageRef = cgImage.decoded(on: context, scale: imageScale)
|
||||
else {
|
||||
return KFCrossPlatformImage(cgImage: cgImage)
|
||||
}
|
||||
|
||||
return KFCrossPlatformImage(cgImage: decodedImageRef)
|
||||
} else {
|
||||
let image = KFCrossPlatformImage(cgImage: cgImage)
|
||||
if backgroundDecode {
|
||||
guard let context = GraphicsContext.current(size: imageSize, scale: imageScale, inverting: true, cgImage: cgImage) else {
|
||||
return image
|
||||
}
|
||||
return image.kf.decoded(on: context)
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePreloadedFrames() {
|
||||
guard preloadingIsNeeded else {
|
||||
return
|
||||
}
|
||||
|
||||
let previousFrame = animatedFrames[previousFrameIndex]
|
||||
animatedFrames[previousFrameIndex] = previousFrame?.placeholderFrame
|
||||
// ensure the image dealloc in main thread
|
||||
defer {
|
||||
if let image = previousFrame?.image {
|
||||
DispatchQueue.main.async {
|
||||
_ = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preloadIndexes(start: currentFrameIndex).forEach { index in
|
||||
guard let currentAnimatedFrame = animatedFrames[index] else { return }
|
||||
if !currentAnimatedFrame.isPlaceholder { return }
|
||||
animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index))
|
||||
}
|
||||
}
|
||||
|
||||
private func incrementCurrentFrameIndex() {
|
||||
let wasLastFrame = isLastFrame
|
||||
currentFrameIndex = increment(frameIndex: currentFrameIndex)
|
||||
if isLastFrame {
|
||||
currentRepeatCount += 1
|
||||
if isReachMaxRepeatCount {
|
||||
isFinished = true
|
||||
|
||||
// Notify the delegate here because the animation is stopping.
|
||||
delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)
|
||||
}
|
||||
} else if wasLastFrame {
|
||||
|
||||
// Notify the delegate that the loop completed
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SafeArray<Element> {
|
||||
private var array: Array<Element> = []
|
||||
private let lock = NSLock()
|
||||
|
||||
subscript(index: Int) -> Element? {
|
||||
get {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return array.indices ~= index ? array[index] : nil
|
||||
}
|
||||
|
||||
set {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
if let newValue = newValue, array.indices ~= index {
|
||||
array[index] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var count : Int {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return array.count
|
||||
}
|
||||
|
||||
func reserveCapacity(_ count: Int) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
array.reserveCapacity(count)
|
||||
}
|
||||
|
||||
func append(_ element: Element) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
array += [element]
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
array = []
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
233
Pods/Kingfisher/Sources/Views/Indicator.swift
generated
Normal file
233
Pods/Kingfisher/Sources/Views/Indicator.swift
generated
Normal file
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// Indicator.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by João D. Moreira on 30/08/16.
|
||||
//
|
||||
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
public typealias IndicatorView = NSView
|
||||
#else
|
||||
import UIKit
|
||||
public typealias IndicatorView = UIView
|
||||
#endif
|
||||
|
||||
/// Represents the activity indicator type which should be added to
|
||||
/// an image view when an image is being downloaded.
|
||||
///
|
||||
/// - none: No indicator.
|
||||
/// - activity: Uses the system activity indicator.
|
||||
/// - image: Uses an image as indicator. GIF is supported.
|
||||
/// - custom: Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol.
|
||||
public enum IndicatorType {
|
||||
/// No indicator.
|
||||
case none
|
||||
/// Uses the system activity indicator.
|
||||
case activity
|
||||
/// Uses an image as indicator. GIF is supported.
|
||||
case image(imageData: Data)
|
||||
/// Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol.
|
||||
case custom(indicator: Indicator)
|
||||
}
|
||||
|
||||
/// An indicator type which can be used to show the download task is in progress.
|
||||
public protocol Indicator {
|
||||
|
||||
/// Called when the indicator should start animating.
|
||||
func startAnimatingView()
|
||||
|
||||
/// Called when the indicator should stop animating.
|
||||
func stopAnimatingView()
|
||||
|
||||
/// Center offset of the indicator. Kingfisher will use this value to determine the position of
|
||||
/// indicator in the super view.
|
||||
var centerOffset: CGPoint { get }
|
||||
|
||||
/// The indicator view which would be added to the super view.
|
||||
var view: IndicatorView { get }
|
||||
|
||||
/// The size strategy used when adding the indicator to image view.
|
||||
/// - Parameter imageView: The super view of indicator.
|
||||
func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy
|
||||
}
|
||||
|
||||
public enum IndicatorSizeStrategy {
|
||||
case intrinsicSize
|
||||
case full
|
||||
case size(CGSize)
|
||||
}
|
||||
|
||||
extension Indicator {
|
||||
|
||||
/// Default implementation of `centerOffset` of `Indicator`. The default value is `.zero`, means that there is
|
||||
/// no offset for the indicator view.
|
||||
public var centerOffset: CGPoint { return .zero }
|
||||
|
||||
/// Default implementation of `centerOffset` of `Indicator`. The default value is `.full`, means that the indicator
|
||||
/// will pin to the same height and width as the image view.
|
||||
public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy {
|
||||
return .full
|
||||
}
|
||||
}
|
||||
|
||||
// Displays a NSProgressIndicator / UIActivityIndicatorView
|
||||
final class ActivityIndicator: Indicator {
|
||||
|
||||
#if os(macOS)
|
||||
private let activityIndicatorView: NSProgressIndicator
|
||||
#else
|
||||
private let activityIndicatorView: UIActivityIndicatorView
|
||||
#endif
|
||||
private var animatingCount = 0
|
||||
|
||||
var view: IndicatorView {
|
||||
return activityIndicatorView
|
||||
}
|
||||
|
||||
func startAnimatingView() {
|
||||
if animatingCount == 0 {
|
||||
#if os(macOS)
|
||||
activityIndicatorView.startAnimation(nil)
|
||||
#else
|
||||
activityIndicatorView.startAnimating()
|
||||
#endif
|
||||
activityIndicatorView.isHidden = false
|
||||
}
|
||||
animatingCount += 1
|
||||
}
|
||||
|
||||
func stopAnimatingView() {
|
||||
animatingCount = max(animatingCount - 1, 0)
|
||||
if animatingCount == 0 {
|
||||
#if os(macOS)
|
||||
activityIndicatorView.stopAnimation(nil)
|
||||
#else
|
||||
activityIndicatorView.stopAnimating()
|
||||
#endif
|
||||
activityIndicatorView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy {
|
||||
return .intrinsicSize
|
||||
}
|
||||
|
||||
init() {
|
||||
#if os(macOS)
|
||||
activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16))
|
||||
activityIndicatorView.controlSize = .small
|
||||
activityIndicatorView.style = .spinning
|
||||
#else
|
||||
let indicatorStyle: UIActivityIndicatorView.Style
|
||||
|
||||
#if os(tvOS)
|
||||
if #available(tvOS 13.0, *) {
|
||||
indicatorStyle = UIActivityIndicatorView.Style.large
|
||||
} else {
|
||||
indicatorStyle = UIActivityIndicatorView.Style.white
|
||||
}
|
||||
#elseif os(xrOS)
|
||||
indicatorStyle = UIActivityIndicatorView.Style.medium
|
||||
#else
|
||||
if #available(iOS 13.0, * ) {
|
||||
indicatorStyle = UIActivityIndicatorView.Style.medium
|
||||
} else {
|
||||
indicatorStyle = UIActivityIndicatorView.Style.gray
|
||||
}
|
||||
#endif
|
||||
|
||||
activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
extension UIActivityIndicatorView.Style {
|
||||
#if compiler(>=5.1)
|
||||
#else
|
||||
static let large = UIActivityIndicatorView.Style.white
|
||||
#if !os(tvOS)
|
||||
static let medium = UIActivityIndicatorView.Style.gray
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - ImageIndicator
|
||||
// Displays an ImageView. Supports gif
|
||||
final class ImageIndicator: Indicator {
|
||||
private let animatedImageIndicatorView: KFCrossPlatformImageView
|
||||
|
||||
var view: IndicatorView {
|
||||
return animatedImageIndicatorView
|
||||
}
|
||||
|
||||
init?(
|
||||
imageData data: Data,
|
||||
processor: ImageProcessor = DefaultImageProcessor.default,
|
||||
options: KingfisherParsedOptionsInfo? = nil)
|
||||
{
|
||||
var options = options ?? KingfisherParsedOptionsInfo(nil)
|
||||
// Use normal image view to show animations, so we need to preload all animation data.
|
||||
if !options.preloadAllAnimationData {
|
||||
options.preloadAllAnimationData = true
|
||||
}
|
||||
|
||||
guard let image = processor.process(item: .data(data), options: options) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
animatedImageIndicatorView = KFCrossPlatformImageView()
|
||||
animatedImageIndicatorView.image = image
|
||||
|
||||
#if os(macOS)
|
||||
// Need for gif to animate on macOS
|
||||
animatedImageIndicatorView.imageScaling = .scaleNone
|
||||
animatedImageIndicatorView.canDrawSubviewsIntoLayer = true
|
||||
#else
|
||||
animatedImageIndicatorView.contentMode = .center
|
||||
#endif
|
||||
}
|
||||
|
||||
func startAnimatingView() {
|
||||
#if os(macOS)
|
||||
animatedImageIndicatorView.animates = true
|
||||
#else
|
||||
animatedImageIndicatorView.startAnimating()
|
||||
#endif
|
||||
animatedImageIndicatorView.isHidden = false
|
||||
}
|
||||
|
||||
func stopAnimatingView() {
|
||||
#if os(macOS)
|
||||
animatedImageIndicatorView.animates = false
|
||||
#else
|
||||
animatedImageIndicatorView.stopAnimating()
|
||||
#endif
|
||||
animatedImageIndicatorView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user