Visual_Novel_iOS/crush/Crush/Src/Components/Photo/PhotoBrowser/BrowseImageZoomView.swift

410 lines
16 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// BrowseImageZoomView.swift
// Crush
//
// Created by Leon on 2025/7/26.
//
import SnapKit
import UIKit
protocol BrowseImageZoomViewDelegate: AnyObject {
func dismisAnimation(_ zoomImageView: UIImageView, toFrame: CGRect)
func singleTapZoomView(_ zoomImageView: UIImageView) -> Bool
func longPressZoomView(_ zoomView: BrowseImageZoomView) -> Bool
func setVCViewBackgroundColor(_ alpha: CGFloat)
}
class BrowseImageZoomView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private let kMaxScale: CGFloat = 3.0
private let kMaxDropHeight: CGFloat = 60
private var imageUrl: String?
private var progress: PhotoBrowseProgressView?
private var noticeButton: UIButton?
private var stateLabel: UILabel?
private var origFrame: CGRect = .zero
var scrollView: UIScrollView!
var imageView: UIImageView!
weak var delegate: BrowseImageZoomViewDelegate?
var displayIndex: Int = 0
var imageModel: PhotoBrowserModel?
init(imageModel: PhotoBrowserModel) {
super.init(frame: .zero)
self.imageModel = imageModel
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
backgroundColor = .clear
scrollView = UIScrollView()
scrollView.backgroundColor = UIColor.black.withAlphaComponent(1.0) // Assuming kBackgroundColor(1) is black
scrollView.clipsToBounds = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
}
addSubview(scrollView)
// Using SnapKit for scrollView layout
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
imageView = UIImageView(frame: CGRect(
x: 0,
y: (UIScreen.main.bounds.height - UIScreen.main.bounds.width) / 2,
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.width
))
imageView.backgroundColor = .clear
imageView.image = imageModel?.placeHolder
scrollView.addSubview(imageView)
addGestures()
reloadImage(imageModel)
}
private func addGestures() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(singleGestureAction(_:)))
addGestureRecognizer(tapGesture)
let doubleGesture = UITapGestureRecognizer(target: self, action: #selector(doubleGestureAction(_:)))
doubleGesture.numberOfTapsRequired = 2
addGestureRecognizer(doubleGesture)
tapGesture.require(toFail: doubleGesture)
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressGestureAction(_:)))
addGestureRecognizer(longPressGesture)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
panGesture.delegate = self
addGestureRecognizer(panGesture)
}
private func resetErrorNoticeView() {
stateLabel?.removeFromSuperview()
stateLabel = nil
noticeButton?.removeFromSuperview()
noticeButton = nil
}
func reloadImage(_ imageModel: PhotoBrowserModel?) {
guard let imageModel = imageModel else { return }
self.imageModel = imageModel
if let image = imageModel.image {
imageView.image = image
updateImageViewFrame()
}
if let url = imageModel.imageUrl {
loadImageWithImageUrl(url, animated: imageModel.sourceRect.size.width > 0)
}
}
private func loadImageWithImageUrl(_ imageUrl: String, animated:Bool = true) {
self.imageUrl = imageUrl
// guard let url = URL(string: imageUrl) else { return }
// if let cacheImage = SDImageCache.shared.imageFromDiskCache(forKey: url.absoluteString) {
// imageView.image = cacheImage
// resetErrorNoticeView()
// setNeedsLayout()
// imageView.setNeedsDisplay()
// updateImageViewFrame()
// return
// }
progress = PhotoBrowseProgressView()
progress?.isHidden = false
addSubview(progress!)
progress?.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 50, height: 50))
make.center.equalToSuperview()
}
imageView.loadImage(imageUrl) { receivedSize, totalSize in
guard self.imageUrl == imageUrl, totalSize > 0 else { return } // let self = self,
DispatchQueue.main.async {
self.progress?.progress = CGFloat(receivedSize) / CGFloat(totalSize)
}
} completionBlock: { result in
guard self.imageUrl == imageUrl else { return } // let self = self,
DispatchQueue.main.async {
self.progress?.removeFromSuperview()
self.progress = nil
switch result {
case let .success(imageResult):
let duration = animated ? 0.25 : 0
UIView.animate(withDuration: duration) {
self.imageView.image = imageResult.image
self.resetErrorNoticeView()
self.setNeedsLayout()
self.imageView.setNeedsDisplay()
self.updateImageViewFrame()
}
case let .failure(error):
dlog("图片加载失败: \(error.localizedDescription)")
let button = UIButton()
button.isUserInteractionEnabled = false
button.setImage(UIImage(named: "icon_notice_white"), for: .normal)
self.addSubview(button)
self.noticeButton = button
button.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 16, height: 16))
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(-36)
}
let errorLabel = UILabel()
errorLabel.backgroundColor = .clear
errorLabel.textColor = .white
errorLabel.textAlignment = .center
errorLabel.clipsToBounds = true
errorLabel.text = NSLocalizedString("image_load_failed", comment: "")
errorLabel.font = .systemFont(ofSize: 14)
errorLabel.numberOfLines = 2
errorLabel.sizeToFit()
self.addSubview(errorLabel)
errorLabel.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: UIScreen.main.bounds.width - 80, height: 30))
make.center.equalToSuperview()
}
self.stateLabel = errorLabel
}
}
}
/*
imageView.sd_setImage(with: url, placeholderImage: imageModel?.placeHolder, options: [.retryFailed, .lowPriority, .handleCookies]) { [weak self] receivedSize, expectedSize, _ in
guard let self = self, self.imageUrl == imageUrl, expectedSize > 0 else { return }
DispatchQueue.main.async {
self.progress?.progress = CGFloat(receivedSize) / CGFloat(expectedSize)
}
} completed: { [weak self] image, error, _, _ in
guard let self = self, self.imageUrl == imageUrl else { return }
self.progress?.removeFromSuperview()
self.progress = nil
if let error = error {
let button = UIButton()
button.isUserInteractionEnabled = false
button.setImage(UIImage(named: "icon_notice_white"), for: .normal)
self.addSubview(button)
self.noticeButton = button
button.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 16, height: 16))
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(-36)
}
let errorLabel = UILabel()
errorLabel.backgroundColor = .clear
errorLabel.textColor = .white
errorLabel.textAlignment = .center
errorLabel.clipsToBounds = true
errorLabel.text = NSLocalizedString("image_load_failed", comment: "")
errorLabel.font = .systemFont(ofSize: 14)
errorLabel.numberOfLines = 2
errorLabel.sizeToFit()
errorLabel.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: UIScreen.main.bounds.width - 80, height: 30))
make.center.equalToSuperview()
}
self.addSubview(errorLabel)
self.stateLabel = errorLabel
} else {
UIView.animate(withDuration: 0.25) {
self.imageView.image = image
self.resetErrorNoticeView()
self.setNeedsLayout()
self.imageView.setNeedsDisplay()
self.updateImageViewFrame()
}
}
}*/
}
private func updateImageViewFrame() {
guard let image = imageView.image, image.size.height > 0, image.size.width > 0 else { return }
let screenHeight = UIScreen.main.bounds.height
let imageRatio = image.size.width / image.size.height
let newWidth = frame.width
let newHeight = frame.width / imageRatio
let newX: CGFloat = 0
let newY = newHeight > screenHeight ? 0 : (screenHeight - newHeight) / 2
let newRect = CGRect(x: newX, y: newY, width: newWidth, height: newHeight)
imageView.frame = newRect
origFrame = newRect
scrollView.contentSize = CGSize(width: newWidth, height: max(newHeight, screenHeight))
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = max(screenHeight / newHeight, kMaxScale)
scrollView.zoomScale = 1.0
}
@objc private func panGestureAction(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .ended:
scrollView.isScrollEnabled = true
if imageView.frame.origin.y - origFrame.origin.y > kMaxDropHeight {
dismisAnimationWithGestureView(recognizer.view)
} else {
resetFrame()
}
case .cancelled:
resetFrame()
case .changed:
let point = recognizer.translation(in: recognizer.view)
updatePanFrame(point)
default:
break
}
}
@objc private func singleGestureAction(_ recognizer: UITapGestureRecognizer) {
if let stateLabel = stateLabel, !stateLabel.isHidden {
retryAction()
return
}
if delegate?.singleTapZoomView(imageView) ?? false {
dismisAnimationWithGestureView(recognizer.view)
}
}
@objc private func doubleGestureAction(_ recognizer: UITapGestureRecognizer) {
if scrollView.zoomScale > scrollView.minimumZoomScale + 0.5 {
scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
} else {
let tapPoint = recognizer.location(in: recognizer.view)
scrollView.zoom(to: zoomRectForScale(2.0, withTapPoint: tapPoint), animated: true)
}
}
@objc private func longPressGestureAction(_ recognizer: UILongPressGestureRecognizer) {
if recognizer.state == .began {
delegate?.longPressZoomView(self)
}
}
private func retryAction() {
resetErrorNoticeView()
if let imageUrl = imageUrl, let imageModel = imageModel {
loadImageWithImageUrl(imageUrl, animated: imageModel.sourceRect.size.width > 0)
}
}
private func updatePanFrame(_ point: CGPoint) {
var scale = 1.0
if point.y >= 0 && point.y < 360 {
scale = 1 - point.y / 410
scrollView.backgroundColor = UIColor.black.withAlphaComponent(1 - point.y / 250.0)
delegate?.setVCViewBackgroundColor(1 - point.y / 200.0)
} else if point.y > 360 {
scale = 1 - 360 / 410.0
}
let radiusWidth = imageView.frame.width - (imageModel?.sourceRect.width ?? 0)
let newWidth = radiusWidth > 10 ? origFrame.width * scale : imageView.frame.width
let newHeight = radiusWidth > 10 ? origFrame.height * scale : imageView.frame.height
let offsetX = origFrame.width - newWidth
imageView.frame = CGRect(
x: origFrame.origin.x + point.x + offsetX / 2.0,
y: origFrame.origin.y + point.y,
width: newWidth,
height: newHeight
)
}
private func resetFrame() {
isUserInteractionEnabled = false
UIView.animate(withDuration: 0.2) { [weak self] in
guard let self = self else { return }
self.imageView.frame = self.origFrame
self.scrollView.backgroundColor = UIColor.black.withAlphaComponent(1.0)
self.delegate?.setVCViewBackgroundColor(1.0)
} completion: { [weak self] _ in
self?.isUserInteractionEnabled = true
}
}
private func dismisAnimationWithGestureView(_ view: UIView?) {
delegate?.dismisAnimation(imageView, toFrame: imageModel?.sourceRect ?? .zero)
isHidden = true
}
private func zoomRectForScale(_ scale: CGFloat, withTapPoint point: CGPoint) -> CGRect {
let touchX = point.x / scrollView.zoomScale
let touchY = point.y / scrollView.zoomScale
let x = touchX + scrollView.contentOffset.x - frame.width / (2 * scale)
let y = touchY + scrollView.contentOffset.y - frame.height / (2 * scale)
return CGRect(x: x, y: y, width: frame.width / scale, height: frame.height / scale)
}
// MARK: - UIScrollViewDelegate
func scrollViewDidZoom(_ scrollView: UIScrollView) {
imageView.center = centerOfScrollViewContent(scrollView)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
isUserInteractionEnabled = false
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
isUserInteractionEnabled = true
}
private func centerOfScrollViewContent(_ scrollView: UIScrollView) -> CGPoint {
let offsetX = scrollView.bounds.width > scrollView.contentSize.width ? (scrollView.bounds.width - scrollView.contentSize.width) * 0.5 : 0
let offsetY = scrollView.bounds.height > scrollView.contentSize.height ? (scrollView.bounds.height - scrollView.contentSize.height) * 0.5 : 0
return CGPoint(x: scrollView.contentSize.width * 0.5 + offsetX, y: scrollView.contentSize.height * 0.5 + offsetY)
}
func prepareForReuse() {
displayIndex = 0
resetErrorNoticeView()
progress?.removeFromSuperview()
progress = nil
imageView.alpha = 1.0
imageView.image = nil
imageModel = nil
delegate = nil
updateImageViewFrame()
}
// MARK: - UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return scrollView.isScrollEnabled
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let pan = gestureRecognizer as? UIPanGestureRecognizer {
let velocity = pan.velocity(in: pan.view)
if abs(velocity.x) < abs(velocity.y) && velocity.y > 0 && scrollView.contentOffset.y < 10 {
scrollView.isScrollEnabled = false
return true
}
}
return false
}
}