LNImagePreviewCell.swift 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. //
  2. // LNImagePreviewCell.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/11.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. protocol LNImagePreviewCellDelegate: NSObject {
  11. func onImagePreviewCellDragToDismiss(cell: LNImagePreviewCell)
  12. }
  13. class LNImagePreviewCell: UICollectionViewCell {
  14. private let scrollView = UIScrollView()
  15. private let imageView = UIImageView()
  16. private let indicator = UIActivityIndicatorView(style: .large)
  17. private var panGesture: UIPanGestureRecognizer?
  18. private var touchBeginPoint: CGPoint = .zero
  19. private var lastMove: CGPoint = .zero
  20. private let dragScaleMin = 0.4
  21. private let dragScaleOffsetY = UIScreen.main.bounds.height * 0.5
  22. private let dragAlphaOffsetY = 150.0
  23. private var curUrl: String?
  24. weak var delegate: LNImagePreviewCellDelegate?
  25. override init(frame: CGRect) {
  26. super.init(frame: frame)
  27. setupViews()
  28. setupGesture()
  29. }
  30. func update(url: String) {
  31. resetZoom()
  32. loadImage(from: url)
  33. }
  34. func resetZoom() {
  35. scrollView.setZoomScale(1.0, animated: false)
  36. }
  37. required init?(coder: NSCoder) {
  38. fatalError("init(coder:) has not been implemented")
  39. }
  40. }
  41. extension LNImagePreviewCell {
  42. private func loadImage(from url: String) {
  43. curUrl = url
  44. let handler: (UIImage?) -> Void = { [weak self] image in
  45. guard let self else { return }
  46. guard curUrl == url else { return }
  47. indicator.stopAnimating()
  48. if let image, let viewController {
  49. if image.size.width / image.size.height > viewController.view.bounds.width / viewController.view.bounds.height {
  50. imageView.snp.remakeConstraints { make in
  51. make.edges.equalToSuperview()
  52. make.width.equalToSuperview()
  53. make.height.equalTo(self.imageView.snp.width).multipliedBy(image.size.height / image.size.width)
  54. }
  55. } else {
  56. imageView.snp.remakeConstraints { make in
  57. make.edges.equalToSuperview()
  58. make.height.equalToSuperview()
  59. make.width.equalTo(self.imageView.snp.height).multipliedBy(image.size.width / image.size.height)
  60. }
  61. }
  62. } else {
  63. imageView.snp.remakeConstraints { make in
  64. make.edges.equalToSuperview()
  65. make.width.height.equalTo(0)
  66. }
  67. }
  68. contentView.layoutIfNeeded()
  69. centerImage()
  70. }
  71. indicator.startAnimating()
  72. if url.hasPrefix("http") {
  73. imageView.sd_setImage(with: URL(string: url)) { image, _,_,_ in
  74. handler(image)
  75. }
  76. } else {
  77. imageView.sd_setImage(with: URL(fileURLWithPath: url)) { image,_,_,_ in
  78. handler(image)
  79. }
  80. }
  81. }
  82. private func centerImage() {
  83. let imageViewSize = imageView.frame.size
  84. let scrollViewSize = scrollView.bounds.size
  85. let verticalInset = (scrollViewSize.height - imageViewSize.height) / 2
  86. let horizontalInset = (scrollViewSize.width - imageViewSize.width) / 2
  87. scrollView.contentInset = UIEdgeInsets(
  88. top: max(verticalInset, 0),
  89. left: max(horizontalInset, 0),
  90. bottom: max(verticalInset, 0),
  91. right: max(horizontalInset, 0)
  92. )
  93. }
  94. private func zoomRect(for scale: CGFloat, center: CGPoint) -> CGRect {
  95. var zoomRect = CGRect.zero
  96. zoomRect.size.height = imageView.frame.size.height / scale
  97. zoomRect.size.width = imageView.frame.size.width / scale
  98. zoomRect.origin.x = center.x - (zoomRect.size.width / 2)
  99. zoomRect.origin.y = center.y - (zoomRect.size.height / 2)
  100. return zoomRect
  101. }
  102. @objc
  103. private func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
  104. let currentScale = scrollView.zoomScale
  105. let targetScale = currentScale == 1.0 ? scrollView.maximumZoomScale : 1.0
  106. // 获取点击位置,缩放到点击点
  107. let point = gesture.location(in: imageView)
  108. let zoomRect = zoomRect(for: targetScale, center: point)
  109. scrollView.zoom(to: zoomRect, animated: true)
  110. }
  111. @objc
  112. private func handlePan(_ gesture: UIPanGestureRecognizer) {
  113. let position = gesture.translation(in: self)
  114. switch gesture.state {
  115. case .began:
  116. touchBeginPoint = position
  117. break
  118. case .changed:
  119. lastMove = gesture.velocity(in: self)
  120. var frame = contentView.frame
  121. frame.origin.y = position.y - touchBeginPoint.y
  122. frame.origin.x = position.x - touchBeginPoint.x
  123. contentView.frame = frame
  124. let progress = (frame.origin.y / dragAlphaOffsetY).bounded(min: 0, max: 1.0)
  125. superview?.backgroundColor = .black.withAlphaComponent(1 - progress)
  126. let scale = 1 - (frame.origin.y / dragScaleOffsetY).bounded(min: 0, max: 1.0) * (1 - dragScaleMin)
  127. imageView.transform = .init(scaleX: scale, y: scale)
  128. break
  129. default:
  130. if lastMove.y > 0 {
  131. var frame = contentView.frame
  132. frame.origin.y = bounds.height
  133. UIView.animate(withDuration: 0.25) { [weak self] in
  134. guard let self else { return }
  135. contentView.frame = frame
  136. superview?.backgroundColor = .clear
  137. } completion: { [weak self] _ in
  138. guard let self else { return }
  139. delegate?.onImagePreviewCellDragToDismiss(cell: self)
  140. }
  141. } else {
  142. UIView.animate(withDuration: 0.25) { [weak self] in
  143. guard let self else { return }
  144. contentView.frame = bounds
  145. superview?.backgroundColor = .black
  146. imageView.transform = .identity
  147. }
  148. }
  149. break
  150. }
  151. }
  152. }
  153. extension LNImagePreviewCell: UIScrollViewDelegate {
  154. func viewForZooming(in scrollView: UIScrollView) -> UIView? {
  155. return imageView
  156. }
  157. func scrollViewDidZoom(_ scrollView: UIScrollView) {
  158. centerImage()
  159. panGesture?.isEnabled = scrollView.zoomScale == 1.0
  160. }
  161. }
  162. extension LNImagePreviewCell: UIGestureRecognizerDelegate {
  163. override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  164. guard let pan = gestureRecognizer as? UIPanGestureRecognizer else {
  165. return true
  166. }
  167. let velocity = pan.velocity(in: self)
  168. return abs(velocity.y) > abs(velocity.x)
  169. }
  170. }
  171. extension LNImagePreviewCell {
  172. private func setupViews() {
  173. scrollView.delegate = self
  174. scrollView.minimumZoomScale = 1.0
  175. scrollView.maximumZoomScale = UIScreen.main.bounds.height / UIScreen.main.bounds.width
  176. scrollView.showsVerticalScrollIndicator = false
  177. scrollView.showsHorizontalScrollIndicator = false
  178. contentView.addSubview(scrollView)
  179. scrollView.snp.makeConstraints { make in
  180. make.edges.equalToSuperview()
  181. }
  182. imageView.contentMode = .scaleAspectFit
  183. imageView.backgroundColor = .clear
  184. scrollView.addSubview(imageView)
  185. imageView.snp.makeConstraints { make in
  186. make.edges.equalToSuperview()
  187. make.width.height.equalTo(0)
  188. }
  189. indicator.color = .white
  190. indicator.hidesWhenStopped = true
  191. contentView.addSubview(indicator)
  192. indicator.snp.makeConstraints { make in
  193. make.center.equalToSuperview()
  194. }
  195. }
  196. private func setupGesture() {
  197. let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
  198. doubleTap.numberOfTapsRequired = 2
  199. scrollView.addGestureRecognizer(doubleTap)
  200. let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
  201. pan.delegate = self
  202. pan.isEnabled = true
  203. pan.maximumNumberOfTouches = 1
  204. contentView.addGestureRecognizer(pan)
  205. panGesture = pan
  206. contentView.onTap { [weak self] in
  207. guard let self else { return }
  208. var frame = contentView.frame
  209. frame.origin.y = bounds.height
  210. UIView.animate(withDuration: 0.25) { [weak self] in
  211. guard let self else { return }
  212. contentView.frame = frame
  213. superview?.backgroundColor = .clear
  214. } completion: { [weak self] _ in
  215. guard let self else { return }
  216. delegate?.onImagePreviewCellDragToDismiss(cell: self)
  217. }
  218. }
  219. }
  220. }