421 lines
14 KiB
Swift
421 lines
14 KiB
Swift
//
|
||
// SPPlayerListViewController.swift
|
||
// MoviaBox
|
||
//
|
||
// Created by 曾觉新 on 2025/4/8.
|
||
//
|
||
|
||
import UIKit
|
||
import AVKit
|
||
|
||
@objc protocol SPPlayerListViewControllerDelegate {
|
||
|
||
///加载新数据
|
||
@objc optional func sp_playerViewControllerLoadNewDataV2(playerViewController: SPPlayerListViewController)
|
||
|
||
///将要加载更多数据
|
||
@objc optional func sp_playerViewControllerShouldLoadMoreData(playerViewController: SPPlayerListViewController) -> Bool
|
||
///加载更多数据
|
||
@objc optional func sp_playerViewControllerLoadMoreData(playerViewController: SPPlayerListViewController)
|
||
///向上加载更多数据
|
||
@objc optional func sp_playerViewControllerLoadUpMoreData(playerViewController: SPPlayerListViewController)
|
||
|
||
///当前展示的发生变化
|
||
@objc optional func sp_playerListViewController(_ viewController: SPPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath)
|
||
|
||
// @objc optional func sp_playerListViewController(_ viewController: SPPlayerListViewController, didScrollFromIndex fromIndex: Int, toIndex: Int)
|
||
///新页面展示完成
|
||
// @objc optional func yd_playerViewController(playerListViewController: BCListPlayerViewController, didShowPlayerPage playerViewController: YDBasePlayerViewController)
|
||
}
|
||
|
||
@objc protocol SPPlayerListViewControllerDataSource {
|
||
|
||
|
||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell
|
||
|
||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int
|
||
|
||
|
||
|
||
}
|
||
|
||
class SPPlayerListViewController: SPViewController {
|
||
|
||
|
||
var contentSize: CGSize {
|
||
return CGSize(width: kSPScreenWidth, height: kSPScreenHeight - kSPTabBarHeight)
|
||
}
|
||
|
||
var PlayerCellClass: SPPlayerListCell.Type {
|
||
return SPPlayerListCell.self
|
||
}
|
||
|
||
weak var delegate: SPPlayerListViewControllerDelegate?
|
||
weak var dataSource: SPPlayerListViewControllerDataSource?
|
||
|
||
private(set) var dataArr: [Any] = []
|
||
var pagination: SPListPaginationModel?
|
||
|
||
///自动下一级
|
||
var autoNextEpisode = false
|
||
|
||
///是否为首次播放
|
||
private(set) var isFirstPlay = true
|
||
|
||
private(set) var viewModel = SPPlayerListViewModel()
|
||
|
||
private(set) var currentIndexPath = IndexPath(row: 0, section: 0)
|
||
|
||
private lazy var collectionViewLayout: UICollectionViewLayout = {
|
||
let layout = UICollectionViewFlowLayout()
|
||
layout.itemSize = contentSize
|
||
layout.minimumInteritemSpacing = 0
|
||
layout.minimumLineSpacing = 0
|
||
return layout
|
||
}()
|
||
|
||
private(set) lazy var collectionView: SPCollectionView = {
|
||
let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||
collectionView.delegate = self
|
||
collectionView.dataSource = self
|
||
collectionView.isPagingEnabled = true
|
||
collectionView.showsVerticalScrollIndicator = false
|
||
collectionView.showsHorizontalScrollIndicator = false
|
||
collectionView.bounces = false
|
||
collectionView.scrollsToTop = false
|
||
PlayerCellClass.registerCell(collectionView: collectionView)
|
||
return collectionView
|
||
}()
|
||
|
||
deinit {
|
||
NotificationCenter.default.removeObserver(self)
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
self.jx_popGestureType = .disabled
|
||
//视频缓存
|
||
do {
|
||
try? KTVHTTPCache.proxyStart()
|
||
}
|
||
|
||
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||
NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
|
||
|
||
|
||
sp_setupUI()
|
||
sp_addActio()
|
||
}
|
||
|
||
override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
}
|
||
|
||
override func viewDidAppear(_ animated: Bool) {
|
||
super.viewDidAppear(animated)
|
||
if getDataCount() > 0 && self.viewModel.isPlaying {
|
||
self.viewModel.currentPlayer?.start()
|
||
// self.play()
|
||
}
|
||
}
|
||
|
||
override func viewDidDisappear(_ animated: Bool) {
|
||
super.viewDidDisappear(animated)
|
||
self.viewModel.currentPlayer?.pause()
|
||
///处理切换页面后,滑动代理scrollViewDidEndDecelerating不执行的问题
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||
guard let self = self else { return }
|
||
self.scrollDidEnd(self.collectionView)
|
||
}
|
||
}
|
||
|
||
|
||
func setDataArr(dataArr: [Any], completer: (() -> Void)?) {
|
||
clearDataArr()
|
||
self.dataArr = dataArr
|
||
reloadData(completion: completer)
|
||
}
|
||
|
||
func addDataArr(dataArr: [Any]) {
|
||
guard dataArr.count > 0 else { return }
|
||
|
||
var indexPaths: [IndexPath] = []
|
||
var startRow = self.dataArr.count
|
||
|
||
dataArr.forEach { _ in
|
||
indexPaths.append(IndexPath(row: startRow, section: 0))
|
||
startRow += 1
|
||
}
|
||
self.dataArr += dataArr
|
||
|
||
CATransaction.setCompletionBlock(nil)
|
||
CATransaction.begin()
|
||
self.collectionView.insertItems(at: indexPaths)
|
||
CATransaction.commit()
|
||
}
|
||
|
||
|
||
func clearDataArr() {
|
||
self.dataArr.removeAll()
|
||
self.viewModel.currentPlayer = nil
|
||
self.currentIndexPath = .init(row: 0, section: 0)
|
||
self.collectionView.contentOffset = .init(x: 0, y: 0)
|
||
self.collectionView.reloadData()
|
||
}
|
||
|
||
|
||
func play() {
|
||
if self.isDidAppear {
|
||
self.viewModel.currentPlayer?.start()
|
||
}
|
||
|
||
self.viewModel.isPlaying = true
|
||
|
||
if getDataCount() - currentIndexPath.row <= 2 {
|
||
self.loadMoreData()
|
||
}
|
||
|
||
if isFirstPlay {
|
||
isFirstPlay = false
|
||
let offset = self.collectionView.contentOffset.y + 0.2
|
||
self.collectionView.setContentOffset(CGPoint(x: 0, y: offset), animated: false)
|
||
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||
let offset = self.collectionView.contentOffset.y
|
||
self.collectionView.setContentOffset(CGPoint(x: 0, y: floor(offset)), animated: false)
|
||
}
|
||
}
|
||
}
|
||
|
||
func pause() {
|
||
self.viewModel.isPlaying = false
|
||
self.viewModel.currentPlayer?.pause()
|
||
}
|
||
|
||
func reloadData(completion: (() -> Void)? = nil) {
|
||
CATransaction.setCompletionBlock { [weak self] in
|
||
guard let self = self else { return }
|
||
let cell = self.collectionView.cellForItem(at: self.currentIndexPath) as? SPPlayerListCell
|
||
self.viewModel.currentPlayer = cell
|
||
|
||
completion?()
|
||
}
|
||
CATransaction.begin()
|
||
self.collectionView.reloadData()
|
||
CATransaction.commit()
|
||
}
|
||
|
||
func getDataCount() -> Int {
|
||
return self.collectionView(self.collectionView, numberOfItemsInSection: 0)
|
||
}
|
||
|
||
func scrollToItem(indexPath: IndexPath, animated: Bool = true, completer: (() -> Void)? = nil) {
|
||
CATransaction.setCompletionBlock { [weak self] in
|
||
guard let self = self else { return }
|
||
if !animated {
|
||
if self.currentIndexPath != indexPath {
|
||
self.skip(indexPath: indexPath)
|
||
} else {
|
||
self.play()
|
||
}
|
||
}
|
||
completer?()
|
||
}
|
||
CATransaction.begin()
|
||
self.collectionView.scrollToItem(at: indexPath, at: .top, animated: animated);
|
||
CATransaction.commit()
|
||
}
|
||
|
||
///当前播放完成 子类可重写
|
||
func currentPlayFinish() {
|
||
if self.autoNextEpisode {
|
||
scrollToNextEpisode()
|
||
}
|
||
}
|
||
|
||
///当前播放进度变更 子类可重写
|
||
func currentPlayTimeDidChange(time: Int) {
|
||
|
||
}
|
||
}
|
||
|
||
extension SPPlayerListViewController {
|
||
private func sp_setupUI() {
|
||
view.addSubview(collectionView)
|
||
|
||
collectionView.snp.makeConstraints { make in
|
||
// make.edges.equalToSuperview()
|
||
// make.center.equalToSuperview()
|
||
make.top.equalToSuperview()
|
||
make.left.equalToSuperview()
|
||
make.width.equalTo(self.contentSize.width)
|
||
make.height.equalTo(self.contentSize.height)
|
||
}
|
||
}
|
||
|
||
private func sp_addActio() {
|
||
self.viewModel.handlePauseOrPlay = { [weak self] in
|
||
self?.clickPauseOrPlay()
|
||
}
|
||
|
||
self.viewModel.handlePlayFinish = { [weak self] in
|
||
self?.currentPlayFinish()
|
||
}
|
||
|
||
self.viewModel.handlePlayTimeDidChange = { [weak self] time in
|
||
|
||
self?.currentPlayTimeDidChange(time: time)
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
extension SPPlayerListViewController {
|
||
///点击播放或暂停
|
||
private func clickPauseOrPlay() {
|
||
if self.viewModel.isPlaying {
|
||
self.pause()
|
||
} else {
|
||
self.play()
|
||
}
|
||
}
|
||
|
||
|
||
///滑动至下一级
|
||
private func scrollToNextEpisode() {
|
||
|
||
var contentOffset = self.collectionView.contentOffset
|
||
|
||
if hasNextEpisode() {
|
||
contentOffset.y = floor(contentOffset.y + self.contentSize.height)
|
||
self.collectionView.setContentOffset(contentOffset, animated: true)
|
||
} else {
|
||
self.viewModel.currentPlayer?.replay()
|
||
}
|
||
}
|
||
|
||
|
||
///是否还有下一级
|
||
private func hasNextEpisode() -> Bool {
|
||
let contentOffset = self.collectionView.contentOffset
|
||
let contentSize = self.collectionView.contentSize
|
||
if contentOffset.y >= contentSize.height - self.contentSize.height {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
|
||
}
|
||
|
||
//MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource --------------
|
||
extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||
|
||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||
var cell: UICollectionViewCell = PlayerCellClass.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
|
||
|
||
if let newCell = self.dataSource?.sp_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) {
|
||
cell = newCell
|
||
}
|
||
|
||
if let cell = cell as? SPPlayerListCell {
|
||
if cell.viewModel == nil {
|
||
cell.viewModel = viewModel
|
||
}
|
||
// let model = dataArr[indexPath.row]
|
||
// cell.model = model
|
||
|
||
}
|
||
|
||
if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath, let playerProtocol = cell as? SPPlayerProtocol {
|
||
self.currentIndexPath = indexPath
|
||
self.viewModel.currentPlayer = playerProtocol
|
||
didChangeIndexPathForVisible()
|
||
}
|
||
|
||
spLog(message: "++++++++\(indexPath)")
|
||
|
||
return cell
|
||
}
|
||
|
||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||
let count = dataArr.count
|
||
|
||
if let newCount = self.dataSource?.sp_playerListViewController(self, collectionView, numberOfItemsInSection: section, oldNumber: count) {
|
||
return newCount
|
||
} else {
|
||
return count
|
||
}
|
||
}
|
||
|
||
//滑动停止
|
||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||
scrollDidEnd(scrollView)
|
||
}
|
||
|
||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||
scrollDidEnd(scrollView)
|
||
}
|
||
|
||
private func scrollDidEnd(_ scrollView: UIScrollView) {
|
||
let offsetY = scrollView.contentOffset.y
|
||
let indexPaths = self.collectionView.indexPathsForVisibleItems
|
||
for indexPath in indexPaths {
|
||
guard let cell = self.collectionView.cellForItem(at: indexPath) else { continue }
|
||
if floor(offsetY) == floor(cell.frame.origin.y) {
|
||
if self.currentIndexPath != indexPath {
|
||
self.skip(indexPath: indexPath)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func skip(indexPath: IndexPath) {
|
||
currentIndexPath = indexPath
|
||
guard let currentPlayer = self.collectionView.cellForItem(at: indexPath) as? SPPlayerProtocol else { return }
|
||
self.viewModel.currentPlayer = currentPlayer
|
||
// currentCell = self.collectionView.cellForItem(at: indexPath) as? BCListPlayerCell
|
||
didChangeIndexPathForVisible()
|
||
self.play()
|
||
}
|
||
|
||
}
|
||
|
||
//MARK: -------------- APP生命周期 --------------
|
||
extension SPPlayerListViewController {
|
||
|
||
@objc func didBecomeActiveNotification() {
|
||
if getDataCount() > 0 && self.viewModel.isPlaying && isDidAppear {
|
||
self.viewModel.currentPlayer?.start()
|
||
}
|
||
}
|
||
|
||
@objc func willResignActiveNotification() {
|
||
self.viewModel.currentPlayer?.pause()
|
||
}
|
||
|
||
|
||
}
|
||
|
||
extension SPPlayerListViewController {
|
||
|
||
private func loadMoreData() {
|
||
let isLoad = self.delegate?.sp_playerViewControllerShouldLoadMoreData?(playerViewController: self)
|
||
if isLoad != false {
|
||
self.delegate?.sp_playerViewControllerLoadMoreData?(playerViewController: self)
|
||
}
|
||
}
|
||
|
||
private func loadUpMoreData() {
|
||
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||
// guard let self = self else { return }
|
||
// }
|
||
self.delegate?.sp_playerViewControllerLoadUpMoreData?(playerViewController: self)
|
||
}
|
||
|
||
private func didChangeIndexPathForVisible() {
|
||
self.delegate?.sp_playerListViewController?(self, didChangeIndexPathForVisible: self.currentIndexPath)
|
||
}
|
||
}
|