mastodon-ios/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageView.swift

224 lines
7.6 KiB
Swift

//
// MediaPreviewImageView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-4-28.
//
import os.log
import func AVFoundation.AVMakeRect
import UIKit
import FLAnimatedImage
final class MediaPreviewImageView: UIScrollView {
let imageView: FLAnimatedImageView = {
let imageView = FLAnimatedImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true
// accessibility
imageView.accessibilityIgnoresInvertColors = true
imageView.isAccessibilityElement = true
return imageView
}()
let doubleTapGestureRecognizer: UITapGestureRecognizer = {
let tapGestureRecognizer = UITapGestureRecognizer()
tapGestureRecognizer.numberOfTapsRequired = 2
return tapGestureRecognizer
}()
private var containerFrame: CGRect?
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension MediaPreviewImageView {
private func _init() {
isUserInteractionEnabled = true
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
bouncesZoom = true
minimumZoomScale = 1.0
maximumZoomScale = 4.0
addSubview(imageView)
doubleTapGestureRecognizer.addTarget(self, action: #selector(MediaPreviewImageView.doubleTapGestureRecognizerHandler(_:)))
imageView.addGestureRecognizer(doubleTapGestureRecognizer)
delegate = self
}
override func layoutSubviews() {
super.layoutSubviews()
guard let image = imageView.image else { return }
setup(image: image, container: self)
}
}
extension MediaPreviewImageView {
@objc private func doubleTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let middleZoomScale = 0.5 * maximumZoomScale
if zoomScale >= middleZoomScale {
setZoomScale(minimumZoomScale, animated: true)
} else {
let center = sender.location(in: imageView)
let zoomRect: CGRect = {
let width = bounds.width / middleZoomScale
let height = bounds.height / middleZoomScale
return CGRect(
x: center.x - 0.5 * width,
y: center.y - 0.5 * height,
width: width,
height: height
)
}()
zoom(to: zoomRect, animated: true)
}
}
}
extension MediaPreviewImageView {
func setup(image: UIImage, container: UIView, forceUpdate: Bool = false) {
guard image.size.width > 0, image.size.height > 0 else { return }
guard container.bounds.width > 0, container.bounds.height > 0 else { return }
// do not setup when frame not change except force update
if containerFrame == container.frame && !forceUpdate {
return
}
containerFrame = container.frame
// reset to normal
zoomScale = minimumZoomScale
let imageViewSize = AVMakeRect(aspectRatio: image.size, insideRect: container.bounds).size
let imageContentInset: UIEdgeInsets = {
if imageViewSize.width == container.bounds.width {
return UIEdgeInsets(top: 0.5 * (container.bounds.height - imageViewSize.height), left: 0, bottom: 0, right: 0)
} else {
return UIEdgeInsets(top: 0, left: 0.5 * (container.bounds.width - imageViewSize.width), bottom: 0, right: 0)
}
}()
imageView.frame = CGRect(origin: .zero, size: imageViewSize)
if imageView.image == nil {
imageView.image = image
}
contentSize = imageViewSize
contentInset = imageContentInset
centerScrollViewContents()
contentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: setup image for container %s", ((#file as NSString).lastPathComponent), #line, #function, container.frame.debugDescription)
}
}
// MARK: - UIScrollViewDelegate
extension MediaPreviewImageView: UIScrollViewDelegate {
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
return false
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
centerScrollViewContents()
// set bounce when zoom in
alwaysBounceVertical = zoomScale > minimumZoomScale
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
// Ref: https://stackoverflow.com/questions/14069571/keep-zoomable-image-in-center-of-uiscrollview
extension MediaPreviewImageView {
private var scrollViewVisibleSize: CGSize {
let contentInset = self.contentInset
let scrollViewSize = bounds.standardized.size
let width = scrollViewSize.width - contentInset.left - contentInset.right
let height = scrollViewSize.height - contentInset.top - contentInset.bottom
return CGSize(width: width, height: height)
}
private var scrollViewCenter: CGPoint {
let scrollViewSize = self.scrollViewVisibleSize
return CGPoint(x: scrollViewSize.width / 2.0,
y: scrollViewSize.height / 2.0)
}
private func centerScrollViewContents() {
guard let image = imageView.image else { return }
let imageViewSize = imageView.frame.size
let imageSize = image.size
var realImageSize: CGSize
if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height {
realImageSize = CGSize(width: imageViewSize.width,
height: imageViewSize.width / imageSize.width * imageSize.height)
} else {
realImageSize = CGSize(width: imageViewSize.height / imageSize.height * imageSize.width,
height: imageViewSize.height)
}
var frame = CGRect.zero
frame.size = realImageSize
imageView.frame = frame
let screenSize = self.frame.size
let offsetX = screenSize.width > realImageSize.width ? (screenSize.width - realImageSize.width) / 2 : 0
let offsetY = screenSize.height > realImageSize.height ? (screenSize.height - realImageSize.height) / 2 : 0
contentInset = UIEdgeInsets(top: offsetY, left: offsetX, bottom: offsetY, right: offsetX)
// The scroll view has zoomed, so you need to re-center the contents
let scrollViewSize = scrollViewVisibleSize
// First assume that image center coincides with the contents box center.
// This is correct when the image is bigger than scrollView due to zoom
var imageCenter = CGPoint(x: contentSize.width / 2.0,
y: contentSize.height / 2.0)
let center = scrollViewCenter
//if image is smaller than the scrollView visible size - fix the image center accordingly
if contentSize.width < scrollViewSize.width {
imageCenter.x = center.x
}
if contentSize.height < scrollViewSize.height {
imageCenter.y = center.y
}
imageView.center = imageCenter
}
}