// // 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 } }