|
|
@@ -35,6 +35,11 @@
|
|
|
#if canImport(UIKit)
|
|
|
import UIKit
|
|
|
import ImageIO
|
|
|
+typealias KFCrossPlatformContentMode = UIView.ContentMode
|
|
|
+#elseif canImport(AppKit)
|
|
|
+import AppKit
|
|
|
+typealias KFCrossPlatformContentMode = NSImageScaling
|
|
|
+#endif
|
|
|
|
|
|
/// Protocol of `AnimatedImageView`.
|
|
|
public protocol AnimatedImageViewDelegate: AnyObject {
|
|
|
@@ -67,7 +72,7 @@ let KFRunLoopModeCommon = RunLoop.Mode.common
|
|
|
///
|
|
|
/// 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 {
|
|
|
+open class AnimatedImageView: KFCrossPlatformImageView {
|
|
|
/// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`.
|
|
|
class TargetProxy {
|
|
|
private weak var target: AnimatedImageView?
|
|
|
@@ -141,8 +146,13 @@ open class AnimatedImageView: UIImageView {
|
|
|
didSet {
|
|
|
if oldValue != repeatCount {
|
|
|
reset()
|
|
|
+ #if os(macOS)
|
|
|
+ needsDisplay = true
|
|
|
+ layer?.setNeedsDisplay()
|
|
|
+ #else
|
|
|
setNeedsDisplay()
|
|
|
layer.setNeedsDisplay()
|
|
|
+ #endif
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -163,9 +173,9 @@ open class AnimatedImageView: UIImageView {
|
|
|
private var isDisplayLinkInitialized: Bool = false
|
|
|
|
|
|
// A display link that keeps calling the `updateFrame` method on every screen refresh.
|
|
|
- private lazy var displayLink: CADisplayLink = {
|
|
|
+ private lazy var displayLink: DisplayLinkCompatible = {
|
|
|
isDisplayLinkInitialized = true
|
|
|
- let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
|
|
|
+ let displayLink = self.compatibleDisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
|
|
|
displayLink.add(to: .main, forMode: runLoopMode)
|
|
|
displayLink.isPaused = true
|
|
|
return displayLink
|
|
|
@@ -177,8 +187,13 @@ open class AnimatedImageView: UIImageView {
|
|
|
if image != oldValue {
|
|
|
reset()
|
|
|
}
|
|
|
+ #if os(macOS)
|
|
|
+ needsDisplay = true
|
|
|
+ layer?.setNeedsDisplay()
|
|
|
+ #else
|
|
|
setNeedsDisplay()
|
|
|
layer.setNeedsDisplay()
|
|
|
+ #endif
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -217,6 +232,94 @@ open class AnimatedImageView: UIImageView {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+#if os(macOS)
|
|
|
+ public override init(frame frameRect: NSRect) {
|
|
|
+ super.init(frame: frameRect)
|
|
|
+ commonInit()
|
|
|
+ }
|
|
|
+
|
|
|
+ public required init?(coder: NSCoder) {
|
|
|
+ super.init(coder: coder)
|
|
|
+ commonInit()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func commonInit() {
|
|
|
+ super.animates = false
|
|
|
+ wantsLayer = true
|
|
|
+ }
|
|
|
+
|
|
|
+ open override var animates: Bool {
|
|
|
+ get {
|
|
|
+ if isDisplayLinkInitialized {
|
|
|
+ return !displayLink.isPaused
|
|
|
+ } else {
|
|
|
+ return super.animates
|
|
|
+ }
|
|
|
+ }
|
|
|
+ set {
|
|
|
+ if newValue {
|
|
|
+ startAnimating()
|
|
|
+ } else {
|
|
|
+ stopAnimating()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ open func startAnimating() {
|
|
|
+ guard let animator = animator else { return }
|
|
|
+ guard !animator.isReachMaxRepeatCount else { return }
|
|
|
+
|
|
|
+ displayLink.isPaused = false
|
|
|
+ }
|
|
|
+
|
|
|
+ open func stopAnimating() {
|
|
|
+ if isDisplayLinkInitialized {
|
|
|
+ displayLink.isPaused = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ open override var wantsUpdateLayer: Bool {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ open override func updateLayer() {
|
|
|
+ if let frame = animator?.currentFrameImage ?? currentFrame, let layer = layer {
|
|
|
+ layer.contents = frame.kf.cgImage
|
|
|
+ layer.contentsScale = frame.kf.scale
|
|
|
+ layer.contentsGravity = determineContentsGravity(for: frame)
|
|
|
+ currentFrame = frame
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func determineContentsGravity(for image: NSImage) -> CALayerContentsGravity {
|
|
|
+ switch imageScaling {
|
|
|
+ case .scaleProportionallyDown:
|
|
|
+ if image.size.width > bounds.width || image.size.height > bounds.height {
|
|
|
+ return .resizeAspect
|
|
|
+ } else {
|
|
|
+ return .center
|
|
|
+ }
|
|
|
+ case .scaleProportionallyUpOrDown:
|
|
|
+ return .resizeAspect
|
|
|
+ case .scaleAxesIndependently:
|
|
|
+ return .resize
|
|
|
+ case .scaleNone:
|
|
|
+ return .center
|
|
|
+ default:
|
|
|
+ return .resizeAspect
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ open override func viewDidMoveToWindow() {
|
|
|
+ super.viewDidMoveToWindow()
|
|
|
+ didMove()
|
|
|
+ }
|
|
|
+
|
|
|
+ open override func viewDidMoveToSuperview() {
|
|
|
+ super.viewDidMoveToSuperview()
|
|
|
+ didMove()
|
|
|
+ }
|
|
|
+#else
|
|
|
override open var isAnimating: Bool {
|
|
|
if isDisplayLinkInitialized {
|
|
|
return !displayLink.isPaused
|
|
|
@@ -255,6 +358,7 @@ open class AnimatedImageView: UIImageView {
|
|
|
super.didMoveToSuperview()
|
|
|
didMove()
|
|
|
}
|
|
|
+#endif
|
|
|
|
|
|
// This is for back compatibility that using regular `UIImageView` to show animated image.
|
|
|
override func shouldPreloadAllAnimation() -> Bool {
|
|
|
@@ -264,19 +368,23 @@ open class AnimatedImageView: UIImageView {
|
|
|
// Reset the animator.
|
|
|
private func reset() {
|
|
|
animator = nil
|
|
|
+ currentFrame = nil
|
|
|
if let image = image, let frameSource = image.kf.frameSource {
|
|
|
#if os(visionOS)
|
|
|
- let targetSize = bounds.scaled(UITraitCollection.current.displayScale).size
|
|
|
+ let scale = UITraitCollection.current.displayScale
|
|
|
+ #elseif os(macOS)
|
|
|
+ let scale = image.recommendedLayerContentsScale(window?.backingScaleFactor ?? 0.0)
|
|
|
+ let contentMode = imageScaling
|
|
|
#else
|
|
|
var scale: CGFloat = 0
|
|
|
-
|
|
|
if #available(iOS 13.0, tvOS 13.0, *) {
|
|
|
scale = UITraitCollection.current.displayScale
|
|
|
} else {
|
|
|
scale = UIScreen.main.scale
|
|
|
}
|
|
|
- let targetSize = bounds.scaled(scale).size
|
|
|
#endif
|
|
|
+ currentFrame = image
|
|
|
+ let targetSize = bounds.scaled(scale).size
|
|
|
let animator = Animator(
|
|
|
frameSource: frameSource,
|
|
|
contentMode: contentMode,
|
|
|
@@ -305,6 +413,11 @@ open class AnimatedImageView: UIImageView {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// If the Animator cannot prepare the next frame in time, `animator.currentFrameImage` will return nil.
|
|
|
+ /// To prevent unexpected blinking in the ImageView, we maintain a cache of the currently displayed frame
|
|
|
+ /// to use as a fallback in such scenarios.
|
|
|
+ private var currentFrame: KFCrossPlatformImage?
|
|
|
+
|
|
|
/// Update the current frame with the displayLink duration.
|
|
|
private func updateFrameIfNeeded() {
|
|
|
guard let animator = animator else {
|
|
|
@@ -334,7 +447,11 @@ open class AnimatedImageView: UIImageView {
|
|
|
|
|
|
animator.shouldChangeFrame(with: duration) { [weak self] hasNewFrame in
|
|
|
if hasNewFrame {
|
|
|
+ #if os(macOS)
|
|
|
+ self?.layer?.setNeedsDisplay()
|
|
|
+ #else
|
|
|
self?.layer.setNeedsDisplay()
|
|
|
+ #endif
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -356,7 +473,7 @@ extension AnimatedImageView {
|
|
|
struct AnimatedFrame {
|
|
|
|
|
|
// The image to display for this frame. Its value is nil when the frame is removed from the buffer.
|
|
|
- let image: UIImage?
|
|
|
+ let image: KFCrossPlatformImage?
|
|
|
|
|
|
// The duration that this frame should remain active.
|
|
|
let duration: TimeInterval
|
|
|
@@ -376,7 +493,7 @@ extension AnimatedImageView {
|
|
|
//
|
|
|
// - parameter image: An optional `UIImage` instance to be assigned to the new frame.
|
|
|
// - returns: An `AnimatedFrame` instance.
|
|
|
- func makeAnimatedFrame(image: UIImage?) -> AnimatedFrame {
|
|
|
+ func makeAnimatedFrame(image: KFCrossPlatformImage?) -> AnimatedFrame {
|
|
|
return AnimatedFrame(image: image, duration: duration)
|
|
|
}
|
|
|
}
|
|
|
@@ -417,7 +534,7 @@ extension AnimatedImageView {
|
|
|
var loopDuration: TimeInterval = 0
|
|
|
|
|
|
/// The image of the current frame.
|
|
|
- public var currentFrameImage: UIImage? {
|
|
|
+ public var currentFrameImage: KFCrossPlatformImage? {
|
|
|
return frame(at: currentFrameIndex)
|
|
|
}
|
|
|
|
|
|
@@ -461,7 +578,11 @@ extension AnimatedImageView {
|
|
|
return maxFrameCount < frameCount - 1
|
|
|
}
|
|
|
|
|
|
+ #if os(macOS)
|
|
|
+ var contentMode = NSImageScaling.scaleAxesIndependently
|
|
|
+ #else
|
|
|
var contentMode = UIView.ContentMode.scaleToFill
|
|
|
+ #endif
|
|
|
|
|
|
private lazy var preloadQueue: DispatchQueue = {
|
|
|
return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
|
|
|
@@ -479,7 +600,7 @@ extension AnimatedImageView {
|
|
|
/// - repeatCount: The repeat count should this animator uses.
|
|
|
/// - preloadQueue: Dispatch queue used for preloading images.
|
|
|
convenience init(imageSource source: CGImageSource,
|
|
|
- contentMode mode: UIView.ContentMode,
|
|
|
+ contentMode mode: KFCrossPlatformContentMode,
|
|
|
size: CGSize,
|
|
|
imageSize: CGSize,
|
|
|
imageScale: CGFloat,
|
|
|
@@ -509,7 +630,7 @@ extension AnimatedImageView {
|
|
|
/// - repeatCount: The repeat count should this animator uses.
|
|
|
/// - preloadQueue: Dispatch queue used for preloading images.
|
|
|
init(frameSource source: ImageFrameSource,
|
|
|
- contentMode mode: UIView.ContentMode,
|
|
|
+ contentMode mode: KFCrossPlatformContentMode,
|
|
|
size: CGSize,
|
|
|
imageSize: CGSize,
|
|
|
imageScale: CGFloat,
|
|
|
@@ -582,13 +703,24 @@ extension AnimatedImageView {
|
|
|
animatedFrames.removeAll()
|
|
|
}
|
|
|
|
|
|
- private func loadFrame(at index: Int) -> UIImage? {
|
|
|
+ private func loadFrame(at index: Int) -> KFCrossPlatformImage? {
|
|
|
let resize = needsPrescaling && size != .zero
|
|
|
let maxSize = resize ? size : nil
|
|
|
guard let cgImage = frameSource.frame(at: index, maxSize: maxSize) else {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
+ #if os(macOS)
|
|
|
+ let image = KFCrossPlatformImage(cgImage: cgImage, size: .zero)
|
|
|
+ if backgroundDecode {
|
|
|
+ guard let context = GraphicsContext.current(size: image.size, scale: image.kf.scale, inverting: false, cgImage: cgImage) else {
|
|
|
+ return image
|
|
|
+ }
|
|
|
+ return image.kf.decoded(on: context)
|
|
|
+ } else {
|
|
|
+ return image
|
|
|
+ }
|
|
|
+ #else
|
|
|
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.
|
|
|
@@ -612,6 +744,7 @@ extension AnimatedImageView {
|
|
|
return image
|
|
|
}
|
|
|
}
|
|
|
+ #endif
|
|
|
}
|
|
|
|
|
|
private func updatePreloadedFrames() {
|
|
|
@@ -725,6 +858,3 @@ class SafeArray<Element> {
|
|
|
}
|
|
|
}
|
|
|
#endif
|
|
|
-#endif
|
|
|
-
|
|
|
-
|