Browse Source

feat: 补充动态发布时的预览功能

陈文艺 1 month ago
parent
commit
d58c972723

+ 2 - 0
Lanu.xcodeproj/project.pbxproj

@@ -102,6 +102,8 @@
 				Common/Views/StarScore/LNStarScoreView.swift,
 				Common/Views/TextView/LNCommonTextView.swift,
 				Common/Views/Toast/LNToastView.swift,
+				Common/Views/VideoPreview/LNVideoPreviewCell.swift,
+				Common/Views/VideoPreview/LNVideoPreviewController.swift,
 				Common/Views/VideoUpload/LNVideoCompressor.swift,
 				Common/Views/VideoUpload/LNVideoUploadView.swift,
 				Common/Voice/LNVoicePlayer.swift,

+ 143 - 0
Lanu/Common/Views/VideoPreview/LNVideoPreviewCell.swift

@@ -0,0 +1,143 @@
+//
+//  LNVideoPreviewCell.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/11.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+protocol LNVideoPreviewCellDelegate: NSObject {
+    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
+    }
+}

+ 139 - 0
Lanu/Common/Views/VideoPreview/LNVideoPreviewController.swift

@@ -0,0 +1,139 @@
+//
+//  LNVideoPreviewController.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/3/4.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+extension UIView {
+    func presentVideoPreview(_ urls: [String], _ targetIndex: Int) {
+        let vc = LNVideoPreviewController()
+        vc.loadVideos(urls: urls, targetIndex: targetIndex)
+        vc.modalPresentationStyle = .overFullScreen
+        viewController?.present(vc, animated: true)
+    }
+}
+
+
+class LNVideoPreviewController: LNViewController {
+    private var videoURLs: [String] = []
+    private var curIndex: Int = 0 {
+        didSet {
+            titleLabel.text = "\(curIndex + 1)/\(videoURLs.count)"
+        }
+    }
+    
+    private var collectionView: UICollectionView?
+    
+    private let fakeBar = LNFakeNaviBar()
+    private let titleLabel = UILabel()
+    
+    func loadVideos(urls: [String], targetIndex: Int) {
+        videoURLs = urls
+        curIndex = targetIndex
+        
+        collectionView?.reloadData()
+        DispatchQueue.main.async { [weak self] in
+            guard let self else { return }
+            collectionView?.scrollToItem(
+                at: .init(row: curIndex, section: 0),
+                at: .centeredHorizontally,
+                animated: false)
+        }
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        showNavigationBar = false
+        setupViews()
+    }
+}
+
+extension LNVideoPreviewController: LNVideoPreviewCellDelegate {
+    func onVideoPreviewCellDragToDismiss(cell: LNVideoPreviewCell) {
+        dismiss(animated: false)
+    }
+}
+
+extension LNVideoPreviewController: UICollectionViewDataSource, UICollectionViewDelegate {
+    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        videoURLs.count
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LNVideoPreviewCell.className, for: indexPath) as! LNVideoPreviewCell
+        cell.update(url: videoURLs[indexPath.row], coverUrl: nil)
+        cell.delegate = self
+        return cell
+    }
+    
+    func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
+        let previewCell = cell as! LNVideoPreviewCell
+        previewCell.stop()
+    }
+    
+    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
+        curIndex = Int(scrollView.contentOffset.x / scrollView.bounds.width)
+    }
+    
+    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
+        curIndex = Int(scrollView.contentOffset.x / scrollView.bounds.width)
+    }
+}
+
+extension LNVideoPreviewController {
+    private func setupViews() {
+        view.backgroundColor = .clear
+        
+        let layout = UICollectionViewFlowLayout()
+        layout.scrollDirection = .horizontal
+        layout.itemSize = view.bounds.size
+        layout.minimumLineSpacing = 0
+        layout.minimumInteritemSpacing = 0
+        
+        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
+        collectionView.isPagingEnabled = true
+        collectionView.showsHorizontalScrollIndicator = false
+        collectionView.backgroundColor = .black
+        collectionView.register(LNVideoPreviewCell.self, forCellWithReuseIdentifier: LNVideoPreviewCell.className)
+        collectionView.dataSource = self
+        collectionView.delegate = self
+        view.addSubview(collectionView)
+        collectionView.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+        self.collectionView = collectionView
+        
+        let fakeBar = buildFakeNavBar()
+        view.addSubview(fakeBar)
+        fakeBar.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+    }
+    
+    private func buildFakeNavBar() -> UIView {
+        fakeBar.showBackButton { [weak self] in
+            guard let self else { return }
+            dismiss(animated: true)
+        }
+        fakeBar.backButton?.setImage(.init(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate), for: .normal)
+        fakeBar.backButton?.tintColor = .fill
+        
+        titleLabel.font = .body_l
+        titleLabel.textColor = .text_1
+        fakeBar.actionView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        return fakeBar
+    }
+}
+

+ 2 - 4
Lanu/Common/Views/VideoUpload/LNVideoUploadView.swift

@@ -27,7 +27,7 @@ extension LNVideoUploadViewDelegate {
 
 class LNVideoUploadView: UIImageView {
     private var curTask: String?
-    private var sourceUrl: URL?
+    private(set) var sourceUrl: URL?
     private var cacheUrl: URL?
     private(set) var videoSize: CGSize?
     private(set) var videoUrl: String?
@@ -239,9 +239,7 @@ extension LNVideoUploadView {
         }
         
         playButton.setImage(.icVoicePlayWithBlackBg, for: .normal)
-        playButton.addAction(UIAction(handler: { [weak self] _ in
-            guard let self else { return }
-        }), for: .touchUpInside)
+        playButton.isUserInteractionEnabled = false
         addSubview(playButton)
         playButton.snp.makeConstraints { make in
             make.center.equalToSuperview()

+ 9 - 0
Lanu/Views/Profile/Feed/LNCreateFeedViewController.swift

@@ -191,6 +191,11 @@ extension LNCreateFeedViewController {
                 if let image {
                     let imageView = buildImageView()
                     imageView.uploadImage(image: image)
+                    imageView.onTap { [weak self, weak imageView] in
+                        guard let self, let imageView else { return }
+                        guard let url = imageView.imageUrl else { return }
+                        view.presentImagePreview(imageUrls, imageUrls.firstIndex(of: url)!)
+                    }
                     photosView.insertArrangedSubview(imageView, at: photosView.arrangedSubviews.count - 1)
                     imageView.snp.makeConstraints { make in
                         make.width.equalTo(imageView.snp.height)
@@ -261,6 +266,10 @@ extension LNCreateFeedViewController {
         container.isHidden = true
         
         videoUploadView.delegate = self
+        videoUploadView.onTap { [weak self] in
+            guard let self else { return }
+            view.presentVideoPreview([videoUploadView.sourceUrl!.absoluteString], 0)
+        }
         videoUploadView.snp.makeConstraints { make in
             make.size.equalTo(CGSize.zero)
         }