| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- //
- // LNImagePreviewCell.swift
- // Lanu
- //
- // Created by OneeChan on 2025/12/11.
- //
- import Foundation
- import UIKit
- import SnapKit
- protocol LNImagePreviewCellDelegate: NSObject {
- func onImagePreviewCellDragToDismiss(cell: LNImagePreviewCell)
- }
- class LNImagePreviewCell: UICollectionViewCell {
- private let scrollView = UIScrollView()
- private let imageView = UIImageView()
- private let indicator = UIActivityIndicatorView(style: .large)
-
- private var panGesture: UIPanGestureRecognizer?
- private var touchBeginPoint: CGPoint = .zero
- private var lastMove: CGPoint = .zero
- private let dragScaleMin = 0.4
- private let dragScaleOffsetY = UIScreen.main.bounds.height * 0.5
- private let dragAlphaOffsetY = 150.0
-
- private var curUrl: String?
-
- weak var delegate: LNImagePreviewCellDelegate?
-
- override init(frame: CGRect) {
- super.init(frame: frame)
- setupViews()
- setupGesture()
- }
-
- func update(url: String) {
- resetZoom()
- loadImage(from: url)
- }
-
- func resetZoom() {
- scrollView.setZoomScale(1.0, animated: false)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension LNImagePreviewCell {
- private func loadImage(from url: String) {
- curUrl = url
-
- let handler: (UIImage?) -> Void = { [weak self] image in
- guard let self else { return }
- guard curUrl == url else { return }
- indicator.stopAnimating()
-
- if let image, let viewController {
- if image.size.width / image.size.height > viewController.view.bounds.width / viewController.view.bounds.height {
- imageView.snp.remakeConstraints { make in
- make.edges.equalToSuperview()
- make.width.equalToSuperview()
- make.height.equalTo(self.imageView.snp.width).multipliedBy(image.size.height / image.size.width)
- }
- } else {
- imageView.snp.remakeConstraints { make in
- make.edges.equalToSuperview()
- make.height.equalToSuperview()
- make.width.equalTo(self.imageView.snp.height).multipliedBy(image.size.width / image.size.height)
- }
- }
- } else {
- imageView.snp.remakeConstraints { make in
- make.edges.equalToSuperview()
- make.width.height.equalTo(0)
- }
- }
- contentView.layoutIfNeeded()
- centerImage()
- }
- indicator.startAnimating()
- if url.hasPrefix("http") {
- imageView.sd_setImage(with: URL(string: url)) { image, _,_,_ in
- handler(image)
- }
- } else {
- imageView.sd_setImage(with: URL(fileURLWithPath: url)) { image,_,_,_ in
- handler(image)
- }
- }
- }
-
- private func centerImage() {
- let imageViewSize = imageView.frame.size
- let scrollViewSize = scrollView.bounds.size
-
- let verticalInset = (scrollViewSize.height - imageViewSize.height) / 2
- let horizontalInset = (scrollViewSize.width - imageViewSize.width) / 2
-
- scrollView.contentInset = UIEdgeInsets(
- top: max(verticalInset, 0),
- left: max(horizontalInset, 0),
- bottom: max(verticalInset, 0),
- right: max(horizontalInset, 0)
- )
- }
-
- private func zoomRect(for scale: CGFloat, center: CGPoint) -> CGRect {
- var zoomRect = CGRect.zero
- zoomRect.size.height = imageView.frame.size.height / scale
- zoomRect.size.width = imageView.frame.size.width / scale
-
- zoomRect.origin.x = center.x - (zoomRect.size.width / 2)
- zoomRect.origin.y = center.y - (zoomRect.size.height / 2)
-
- return zoomRect
- }
-
- @objc
- private func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
- let currentScale = scrollView.zoomScale
- let targetScale = currentScale == 1.0 ? scrollView.maximumZoomScale : 1.0
-
- // 获取点击位置,缩放到点击点
- let point = gesture.location(in: imageView)
- let zoomRect = zoomRect(for: targetScale, center: point)
-
- scrollView.zoom(to: zoomRect, animated: true)
- }
-
- @objc
- private func handlePan(_ gesture: UIPanGestureRecognizer) {
- let position = gesture.translation(in: self)
-
- switch gesture.state {
- case .began:
- touchBeginPoint = position
- break
- case .changed:
- lastMove = gesture.velocity(in: self)
- var frame = contentView.frame
- frame.origin.y = position.y - touchBeginPoint.y
- frame.origin.x = position.x - touchBeginPoint.x
- contentView.frame = frame
-
- let progress = (frame.origin.y / dragAlphaOffsetY).bounded(min: 0, max: 1.0)
- superview?.backgroundColor = .black.withAlphaComponent(1 - progress)
-
- let scale = 1 - (frame.origin.y / dragScaleOffsetY).bounded(min: 0, max: 1.0) * (1 - dragScaleMin)
- imageView.transform = .init(scaleX: scale, y: scale)
- break
- default:
- if lastMove.y > 0 {
- var frame = contentView.frame
- frame.origin.y = bounds.height
-
- UIView.animate(withDuration: 0.25) { [weak self] in
- guard let self else { return }
- contentView.frame = frame
-
- superview?.backgroundColor = .clear
- } completion: { [weak self] _ in
- guard let self else { return }
- delegate?.onImagePreviewCellDragToDismiss(cell: self)
- }
- } else {
- UIView.animate(withDuration: 0.25) { [weak self] in
- guard let self else { return }
- contentView.frame = bounds
-
- superview?.backgroundColor = .black
- imageView.transform = .identity
- }
- }
- break
- }
- }
- }
- extension LNImagePreviewCell: UIScrollViewDelegate {
- func viewForZooming(in scrollView: UIScrollView) -> UIView? {
- return imageView
- }
-
- func scrollViewDidZoom(_ scrollView: UIScrollView) {
- centerImage()
- panGesture?.isEnabled = scrollView.zoomScale == 1.0
- }
- }
- extension LNImagePreviewCell: UIGestureRecognizerDelegate {
- override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
- guard let pan = gestureRecognizer as? UIPanGestureRecognizer else {
- return true
- }
-
- let velocity = pan.velocity(in: self)
- return abs(velocity.y) > abs(velocity.x)
- }
- }
- extension LNImagePreviewCell {
- private func setupViews() {
- scrollView.delegate = self
- scrollView.minimumZoomScale = 1.0
- scrollView.maximumZoomScale = UIScreen.main.bounds.height / UIScreen.main.bounds.width
- scrollView.showsVerticalScrollIndicator = false
- scrollView.showsHorizontalScrollIndicator = false
- contentView.addSubview(scrollView)
- scrollView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- imageView.contentMode = .scaleAspectFit
- imageView.backgroundColor = .clear
- scrollView.addSubview(imageView)
- imageView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- make.width.height.equalTo(0)
- }
-
- indicator.color = .white
- indicator.hidesWhenStopped = true
- contentView.addSubview(indicator)
- indicator.snp.makeConstraints { make in
- make.center.equalToSuperview()
- }
- }
-
- private func setupGesture() {
- let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
- doubleTap.numberOfTapsRequired = 2
- scrollView.addGestureRecognizer(doubleTap)
-
- let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
- pan.delegate = self
- pan.isEnabled = true
- pan.maximumNumberOfTouches = 1
- contentView.addGestureRecognizer(pan)
- panGesture = pan
-
- contentView.onTap { [weak self] in
- guard let self else { return }
- var frame = contentView.frame
- frame.origin.y = bounds.height
-
- UIView.animate(withDuration: 0.25) { [weak self] in
- guard let self else { return }
- contentView.frame = frame
-
- superview?.backgroundColor = .clear
- } completion: { [weak self] _ in
- guard let self else { return }
- delegate?.onImagePreviewCellDragToDismiss(cell: self)
- }
- }
- }
- }
|