parent
f5e9f71586
commit
9768097488
|
@ -17,6 +17,7 @@ struct ImageGrayscalifier {
|
|||
static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? {
|
||||
if Preferences.shared.grayscaleImages,
|
||||
let source = image.cgImage {
|
||||
// todo: should this return the original image if conversion fails?
|
||||
return convert(url: url, cgImage: source)
|
||||
} else {
|
||||
return image
|
||||
|
|
|
@ -11,10 +11,12 @@ import Pachyderm
|
|||
|
||||
class AttachmentPreviewViewController: UIViewController {
|
||||
|
||||
let attachment: Attachment
|
||||
private let attachment: Attachment
|
||||
private let sourceView: AttachmentView
|
||||
|
||||
init(attachment: Attachment) {
|
||||
self.attachment = attachment
|
||||
init(sourceView: AttachmentView) {
|
||||
self.attachment = sourceView.attachment
|
||||
self.sourceView = sourceView
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
@ -29,7 +31,9 @@ class AttachmentPreviewViewController: UIViewController {
|
|||
let imageView: UIImageView
|
||||
if attachment.url.pathExtension == "gif" {
|
||||
let gifView = GIFImageView(image: image)
|
||||
gifView.animate(withGIFData: data)
|
||||
let controller = sourceView.gifController ?? GIFController(gifData: data)
|
||||
controller.attach(to: gifView)
|
||||
controller.startAnimating()
|
||||
imageView = gifView
|
||||
} else {
|
||||
imageView = UIImageView(image: image)
|
||||
|
|
|
@ -41,14 +41,6 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
return animationSourceView?.image
|
||||
}
|
||||
}
|
||||
var animationGifData: Data? {
|
||||
let attachment = attachments[currentIndex]
|
||||
if attachment.url.pathExtension == "gif" {
|
||||
return ImageCache.attachments.getData(attachment.url)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||
|
|
|
@ -12,7 +12,6 @@ import AVFoundation
|
|||
|
||||
protocol LargeImageContentView: UIView {
|
||||
var animationImage: UIImage? { get }
|
||||
var animationGifData: Data? { get }
|
||||
var activityItemsForSharing: [Any] { get }
|
||||
func grayscaleStateChanged()
|
||||
}
|
||||
|
@ -20,7 +19,6 @@ protocol LargeImageContentView: UIView {
|
|||
class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
||||
|
||||
var animationImage: UIImage? { image! }
|
||||
let animationGifData: Data?
|
||||
|
||||
var activityItemsForSharing: [Any] {
|
||||
[image!]
|
||||
|
@ -28,22 +26,12 @@ class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
|||
|
||||
private var sourceData: Data?
|
||||
|
||||
convenience init(sourceData data: Data, isGif: Bool) {
|
||||
self.init(image: UIImage(data: data)!, gifData: isGif ? data : nil)
|
||||
|
||||
self.sourceData = data
|
||||
}
|
||||
|
||||
init(image: UIImage, gifData: Data?) {
|
||||
self.animationGifData = gifData
|
||||
|
||||
init(image: UIImage) {
|
||||
super.init(image: image)
|
||||
|
||||
contentMode = .scaleAspectFit
|
||||
|
||||
if let data = gifData {
|
||||
self.animate(withGIFData: data)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -68,9 +56,35 @@ class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
|||
}
|
||||
}
|
||||
|
||||
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||
var animationImage: UIImage? { image }
|
||||
|
||||
var activityItemsForSharing: [Any] {
|
||||
// todo: should gifs share the data?
|
||||
[image].compactMap { $0 }
|
||||
}
|
||||
|
||||
init(gifController: GIFController) {
|
||||
super.init(image: gifController.lastFrame?.image)
|
||||
|
||||
contentMode = .scaleAspectFit
|
||||
|
||||
gifController.attach(to: self)
|
||||
// todo: doing this in the init feels wrong
|
||||
gifController.startAnimating()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func grayscaleStateChanged() {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
|
||||
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||
private(set) var animationImage: UIImage?
|
||||
var animationGifData: Data? { nil }
|
||||
var activityItemsForSharing: [Any] {
|
||||
// todo: what should we share for gifvs?
|
||||
// some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL?
|
||||
|
|
|
@ -13,7 +13,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
weak var animationSourceView: UIImageView?
|
||||
var largeImageController: LargeImageViewController? { self }
|
||||
var animationImage: UIImage? { contentView.animationImage }
|
||||
var animationGifData: Data? { contentView.animationGifData }
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
|
|
|
@ -40,7 +40,6 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
weak var animationSourceView: UIImageView?
|
||||
var largeImageController: LargeImageViewController? { largeImageVC }
|
||||
var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
|
||||
var animationGifData: Data? { largeImageVC?.animationGifData }
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||
|
@ -93,6 +92,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
|
||||
// always load full resolution from disk for large image, in case the cache is scaled
|
||||
if let entry = cache.get(url, loadOriginal: true) {
|
||||
// todo: if load original is true, is there any way entry.data could be nil?
|
||||
// feels like the data param of createLargeImage shouldn't be optional
|
||||
createLargeImage(data: entry.data, image: entry.image, url: url)
|
||||
} else {
|
||||
createPreview()
|
||||
|
@ -126,19 +127,31 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
guard !loaded else { return }
|
||||
loaded = true
|
||||
|
||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||
let gifData = url.pathExtension == "gif" ? data : nil
|
||||
createLargeImage(image: transformedImage, gifData: gifData)
|
||||
}
|
||||
}
|
||||
let content: LargeImageContentView
|
||||
|
||||
private func createLargeImage(image: UIImage, gifData: Data?) {
|
||||
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
|
||||
|
||||
if let existing = largeImageVC {
|
||||
existing.contentView = imageView
|
||||
// todo: p sure grayscaling gifs has never worked
|
||||
if url.pathExtension == "gif", let data = data {
|
||||
// todo: pulling the gif controller out of the source view feels icky
|
||||
// is it possible for the source view's gif controller to have different data than we just got?
|
||||
// should this be a property set by the animation controller instead?
|
||||
let gifController = (animationSourceView as? GIFImageView)?.gifController ?? GIFController(gifData: data)
|
||||
content = LargeImageGifContentView(gifController: gifController)
|
||||
} else {
|
||||
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
|
||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||
content = LargeImageImageContentView(image: transformedImage)
|
||||
} else {
|
||||
content = LargeImageImageContentView(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
setContent(content)
|
||||
}
|
||||
|
||||
private func setContent(_ content: LargeImageContentView) {
|
||||
if let existing = largeImageVC {
|
||||
existing.contentView = content
|
||||
} else {
|
||||
largeImageVC = LargeImageViewController(contentView: content, description: imageDescription, sourceView: animationSourceView)
|
||||
largeImageVC!.initialControlsVisible = initialControlsVisible
|
||||
largeImageVC!.shrinkGestureEnabled = false
|
||||
embedChild(largeImageVC!)
|
||||
|
@ -154,7 +167,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
||||
image = grayscale
|
||||
}
|
||||
self.createLargeImage(image: image, gifData: nil)
|
||||
setContent(LargeImageImageContentView(image: image))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ protocol LargeImageAnimatableViewController: UIViewController {
|
|||
var animationSourceView: UIImageView? { get }
|
||||
var largeImageController: LargeImageViewController? { get }
|
||||
var animationImage: UIImage? { get }
|
||||
var animationGifData: Data? { get }
|
||||
var dismissInteractionController: LargeImageInteractionController? { get }
|
||||
var isInteractivelyAnimatingDismissal: Bool { get set }
|
||||
}
|
||||
|
@ -86,8 +85,8 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
|||
|
||||
let imageView = GIFImageView(frame: sourceFrame)
|
||||
imageView.image = image
|
||||
if let gifData = toVC.animationGifData {
|
||||
imageView.animate(withGIFData: gifData)
|
||||
if let gifController = (sourceView as? GIFImageView)?.gifController {
|
||||
gifController.attach(to: imageView)
|
||||
}
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.cornerRadius = sourceView.layer.cornerRadius
|
||||
|
|
|
@ -56,8 +56,10 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
|
|||
|
||||
let imageView = GIFImageView(frame: originalFrame)
|
||||
imageView.image = image
|
||||
if let gifData = fromVC.animationGifData {
|
||||
imageView.animate(withGIFData: gifData)
|
||||
if let gifController = (sourceView as? GIFImageView)?.gifController {
|
||||
gifController.attach(to: imageView)
|
||||
// todo: this might not be necessary, the large image content view should have started it
|
||||
gifController.startAnimating()
|
||||
}
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.cornerRadius = 0
|
||||
|
|
|
@ -29,14 +29,6 @@ class AttachmentView: GIFImageView {
|
|||
private var attachmentRequest: ImageCache.Request?
|
||||
private var source: Source?
|
||||
|
||||
var gifData: Data? {
|
||||
switch source {
|
||||
case let .gifData(_, data):
|
||||
return data
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
private var autoplayGifs: Bool {
|
||||
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
|
||||
}
|
||||
|
@ -91,11 +83,13 @@ class AttachmentView: GIFImageView {
|
|||
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
|
||||
DispatchQueue.main.async {
|
||||
if self.attachment.kind == .image,
|
||||
let gifData = self.gifData {
|
||||
let gifController = self.gifController {
|
||||
if self.autoplayGifs && !self.isAnimatingGIF {
|
||||
self.animate(withGIFData: gifData)
|
||||
gifController.attach(to: self)
|
||||
gifController.startAnimating()
|
||||
} else if !self.autoplayGifs && self.isAnimatingGIF {
|
||||
self.stopAnimatingGIF()
|
||||
// detach instead of stopping so that any other attached gif views keep animating
|
||||
self.detachGIFController()
|
||||
}
|
||||
} else if self.attachment.kind == .gifv,
|
||||
let gifvView = self.gifvView {
|
||||
|
@ -166,13 +160,18 @@ class AttachmentView: GIFImageView {
|
|||
}
|
||||
if self.attachment.url.pathExtension == "gif" {
|
||||
self.source = .gifData(attachmentURL, data)
|
||||
if self.autoplayGifs {
|
||||
let controller = GIFController(gifData: data)
|
||||
DispatchQueue.main.async {
|
||||
self.animate(withGIFData: data)
|
||||
controller.attach(to: self)
|
||||
if self.autoplayGifs {
|
||||
controller.startAnimating()
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if !self.autoplayGifs {
|
||||
self.displayImage()
|
||||
}
|
||||
|
||||
} else {
|
||||
self.source = .imageData(attachmentURL, data)
|
||||
self.displayImage()
|
||||
|
@ -322,7 +321,7 @@ extension AttachmentView: UIContextMenuInteractionDelegate {
|
|||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
|
||||
if self.attachment.kind == .image {
|
||||
return AttachmentPreviewViewController(attachment: self.attachment)
|
||||
return AttachmentPreviewViewController(sourceView: self)
|
||||
} else if self.attachment.kind == .gifv {
|
||||
let vc = GifvAttachmentViewController(attachment: self.attachment)
|
||||
vc.preferredContentSize = self.image?.size ?? .zero
|
||||
|
|
|
@ -10,25 +10,82 @@ import UIKit
|
|||
|
||||
class GIFImageView: UIImageView {
|
||||
|
||||
private(set) var isAnimatingGIF: Bool = false
|
||||
private var shouldStopAnimatingGIF = false
|
||||
fileprivate(set) var gifController: GIFController? = nil
|
||||
var isAnimatingGIF: Bool { gifController?.state == .playing }
|
||||
|
||||
/// Detaches the current GIF controller from this view.
|
||||
/// If this view is the GIF controller's only one, it will stop itself.
|
||||
func detachGIFController() {
|
||||
gifController?.detach(from: self)
|
||||
}
|
||||
|
||||
func animate(withGIFData data: Data) {
|
||||
CGAnimateImageDataWithBlock(data as CFData, nil) { [weak self] (frameIndex, frame, stop) in
|
||||
}
|
||||
|
||||
/// A `GIFController` controls the animation of one or more `GIFImageView`s.
|
||||
class GIFController {
|
||||
|
||||
// GIFImageView strongly holds the controller so that when the last view detaches, the controller is freed
|
||||
private var imageViews = WeakArray<GIFImageView>()
|
||||
|
||||
private(set) var gifData: Data
|
||||
private(set) var state: State = .stopped
|
||||
private(set) var lastFrame: (image: UIImage, index: Int)? = nil
|
||||
|
||||
init(gifData: Data) {
|
||||
self.gifData = gifData
|
||||
}
|
||||
|
||||
/// Attaches another view to this controller, letting it play back alongside the others.
|
||||
/// Immediately brings it into sync with the others, setting the last frame if there was one.
|
||||
func attach(to view: GIFImageView) {
|
||||
imageViews.append(view)
|
||||
view.gifController = self
|
||||
|
||||
if let lastFrame = lastFrame {
|
||||
view.image = lastFrame.image
|
||||
}
|
||||
}
|
||||
|
||||
/// Detaches the given view from this controller.
|
||||
/// If no views attached views remain, the last strong reference to this controller is nilled out
|
||||
/// and image animation will stop at the next CGAnimateImageDataWithBlock callback.
|
||||
func detach(from view: GIFImageView) {
|
||||
// todo: does === work the way i want here
|
||||
imageViews.removeAll(where: { $0 === view })
|
||||
view.gifController = nil
|
||||
}
|
||||
|
||||
func startAnimating() {
|
||||
guard state.shouldStop else { return }
|
||||
|
||||
state = .playing
|
||||
|
||||
CGAnimateImageDataWithBlock(gifData as CFData, nil) { [weak self] (frameIndex, cgImage, stop) in
|
||||
guard let self = self else {
|
||||
stop.pointee = true
|
||||
return
|
||||
}
|
||||
self.image = UIImage(cgImage: frame)
|
||||
stop.pointee = self.shouldStopAnimatingGIF
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
self.lastFrame = (image, frameIndex)
|
||||
for case let .some(view) in self.imageViews {
|
||||
view.image = image
|
||||
}
|
||||
stop.pointee = self.state.shouldStop
|
||||
}
|
||||
isAnimatingGIF = true
|
||||
}
|
||||
|
||||
func stopAnimatingGIF() {
|
||||
shouldStopAnimatingGIF = true
|
||||
isAnimatingGIF = false
|
||||
func stopAnimating() {
|
||||
guard state == .playing else { return }
|
||||
|
||||
state = .stopping
|
||||
}
|
||||
|
||||
enum State: Equatable {
|
||||
case stopped, playing, stopping
|
||||
|
||||
var shouldStop: Bool {
|
||||
self == .stopped || self == .stopping
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue