// // LNVideoPreviewCell.swift // Lanu // // Created by OneeChan on 2025/12/11. // import Foundation import UIKit import SnapKit protocol LNVideoPreviewCellDelegate: AnyObject { func onVideoPreviewCellDragToDismiss(cell: LNVideoPreviewCell) } class LNVideoPreviewCell: UICollectionViewCell { private let videoPlayer = LNVideoPlayerView() 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: LNVideoPreviewCellDelegate? override init(frame: CGRect) { super.init(frame: frame) setupViews() setupGesture() } func update(url: String, coverUrl: String?) { indicator.startAnimating() videoPlayer.loadVideo(url, coverUrl: coverUrl) } func stop() { videoPlayer.stop() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension LNVideoPreviewCell { @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) videoPlayer.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?.onVideoPreviewCellDragToDismiss(cell: self) } } else { UIView.animate(withDuration: 0.25) { [weak self] in guard let self else { return } contentView.frame = bounds superview?.backgroundColor = .black videoPlayer.transform = .identity } } break } } } extension LNVideoPreviewCell: LNVideoPlayerViewDelegate { func onVideoDidLoad(view: LNVideoPlayerView) { indicator.stopAnimating() videoPlayer.start() } } extension LNVideoPreviewCell: 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 LNVideoPreviewCell { private func setupViews() { videoPlayer.showCover = false videoPlayer.loop = true videoPlayer.delegate = self videoPlayer.backgroundColor = .clear videoPlayer.playButtonSize = 56 contentView.addSubview(videoPlayer) videoPlayer.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.verticalEdges.equalToSuperview() } } private func setupGesture() { let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) pan.delegate = self pan.isEnabled = true pan.maximumNumberOfTouches = 1 contentView.addGestureRecognizer(pan) panGesture = pan } }