|
@@ -0,0 +1,261 @@
|
|
|
+//
|
|
|
+// MVBanner.swift
|
|
|
+// MusicVideoPlus
|
|
|
+//
|
|
|
+// Created by ak on 2021/6/4.
|
|
|
+//
|
|
|
+
|
|
|
+import Foundation
|
|
|
+
|
|
|
+import UIKit
|
|
|
+
|
|
|
+public protocol MVBannerDataSource: NSObjectProtocol {
|
|
|
+ func numberOfItems() -> Int
|
|
|
+ func banner(_ banner: MVBanner, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
|
|
|
+}
|
|
|
+
|
|
|
+@objc public protocol MVBannerDelegate: NSObjectProtocol {
|
|
|
+ @objc optional func banner(_ banner: MVBanner, didSelectItemAt indexPath: IndexPath)
|
|
|
+ @objc optional func pageControl(currentPage page: Int)
|
|
|
+}
|
|
|
+
|
|
|
+private let MVInsetCount: Int = 2
|
|
|
+public class MVBanner: UIView {
|
|
|
+ private var currentIndex: Int = MVInsetCount
|
|
|
+ private var collectionView: UICollectionView!
|
|
|
+ private var flowLayout: MVBannerFlowLayout!
|
|
|
+ public weak var dataSource: MVBannerDataSource?
|
|
|
+ public weak var delegate: MVBannerDelegate?
|
|
|
+
|
|
|
+ public var isAutoPlay: Bool = true
|
|
|
+ public var autoTimeInterval: TimeInterval = 0
|
|
|
+ public var isShowPageControl: Bool = true {
|
|
|
+ didSet {
|
|
|
+ self.pageControl.isHidden = !self.isShowPageControl
|
|
|
+ }
|
|
|
+ }
|
|
|
+ public override var backgroundColor: UIColor? {
|
|
|
+ didSet {
|
|
|
+ self.collectionView.backgroundColor = self.backgroundColor
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private(set) lazy var pageControl: UIPageControl = {
|
|
|
+ let pageControl = UIPageControl()
|
|
|
+ pageControl.pageIndicatorTintColor = UIColor.black
|
|
|
+ pageControl.currentPageIndicatorTintColor = UIColor.white
|
|
|
+ pageControl.hidesForSinglePage = true
|
|
|
+ pageControl.addTarget(self, action: #selector(self.pageControlChanged(_:)), for: .valueChanged)
|
|
|
+ return pageControl
|
|
|
+ }()
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ self.bannerStop()
|
|
|
+ }
|
|
|
+
|
|
|
+ public required init(frame: CGRect = .zero, margin: CGFloat = 0, lineSpacing: CGFloat = 0, minScale: Scale = Scale()) {
|
|
|
+ super.init(frame: frame)
|
|
|
+ self.flowLayout = MVBannerFlowLayout(margin: margin, lineSpacing: lineSpacing, minScale: minScale)
|
|
|
+ self.setupSubviews()
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ public override func layoutSubviews() {
|
|
|
+ super.layoutSubviews()
|
|
|
+ self.collectionView.frame = self.bounds
|
|
|
+ var size = self.pageControl.size(forNumberOfPages: self.pageControl.numberOfPages)
|
|
|
+ size.width = min(self.flowLayout.itemSize.width, size.width)
|
|
|
+ self.pageControl.frame = CGRect(origin: .zero, size: size)
|
|
|
+ var center = self.center
|
|
|
+ center.y = self.flowLayout.itemSize.height - self.pageControl.frame.height * 0.5
|
|
|
+ self.pageControl.center = center
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setupSubviews() {
|
|
|
+ self.collectionView = UICollectionView(frame: self.bounds, collectionViewLayout: self.flowLayout)
|
|
|
+ self.collectionView.backgroundColor = .clear
|
|
|
+ self.collectionView.isScrollEnabled = true
|
|
|
+ self.collectionView.isPagingEnabled = false
|
|
|
+ self.collectionView.showsVerticalScrollIndicator = false
|
|
|
+ self.collectionView.showsHorizontalScrollIndicator = false
|
|
|
+ self.collectionView.dataSource = self
|
|
|
+ self.collectionView.delegate = self
|
|
|
+ self.addSubview(self.collectionView)
|
|
|
+
|
|
|
+ self.addSubview(self.pageControl)
|
|
|
+
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+fileprivate extension MVBanner {
|
|
|
+ func realNumberOfItems() -> Int {
|
|
|
+ return self.dataSource?.numberOfItems() ?? 0
|
|
|
+ }
|
|
|
+ func numberOfItems() -> Int {
|
|
|
+ let count = self.dataSource?.numberOfItems() ?? 0
|
|
|
+ self.pageControl.numberOfPages = count
|
|
|
+ guard count > 1 else { return count }
|
|
|
+ return count + MVInsetCount * 2
|
|
|
+ }
|
|
|
+ func realIndex(index: Int) -> Int {
|
|
|
+ let count = self.dataSource?.numberOfItems() ?? 0
|
|
|
+ guard count > 1 else { return count }
|
|
|
+ return (index + count - MVInsetCount) % count
|
|
|
+ }
|
|
|
+ func realIndexPath(index: Int) -> IndexPath {
|
|
|
+ return IndexPath(item: self.realIndex(index: index), section: 0)
|
|
|
+ }
|
|
|
+ func index(realIndex: Int) -> Int {
|
|
|
+ let count = self.dataSource?.numberOfItems() ?? 0
|
|
|
+ guard count > 1 else { return 0 }
|
|
|
+ return realIndex + MVInsetCount
|
|
|
+ }
|
|
|
+ func indexPath(realIndex: Int) -> IndexPath {
|
|
|
+ return IndexPath(item: self.index(realIndex: realIndex), section: 0)
|
|
|
+ }
|
|
|
+ func setCurrentPage(_ page: Int) {
|
|
|
+ if (delegate?.responds(to: #selector(delegate?.pageControl(currentPage:))) ?? false) {
|
|
|
+ self.delegate?.pageControl?(currentPage: page)
|
|
|
+ } else {
|
|
|
+ if self.pageControl.currentPage != page {
|
|
|
+ self.pageControl.currentPage = page
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ func checkRealOutOfBounds() {
|
|
|
+ if self.currentIndex <= (MVInsetCount - 1) {
|
|
|
+ self.currentIndex = self.realNumberOfItems() + MVInsetCount - 1
|
|
|
+ self.scrollToItem(at: self.currentIndex, animated: false)
|
|
|
+ }
|
|
|
+ else if self.currentIndex >= (self.realNumberOfItems() + MVInsetCount) {
|
|
|
+ self.currentIndex = MVInsetCount
|
|
|
+ self.scrollToItem(at: self.currentIndex, animated: false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ func bannerPlay() {
|
|
|
+ self.bannerStop()
|
|
|
+ if(autoTimeInterval > 0){
|
|
|
+ self.perform(#selector(self.bannerScrollNext), with: nil, afterDelay: self.autoTimeInterval)
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ func bannerStop() {
|
|
|
+ NSObject.cancelPreviousPerformRequests(withTarget: self)
|
|
|
+ }
|
|
|
+ @objc func pageControlChanged(_ sender: UIPageControl) {
|
|
|
+ self.bannerStop()
|
|
|
+ self.scrollToItem(realAt: sender.currentPage, animated: true)
|
|
|
+ }
|
|
|
+ @objc func bannerScrollNext() {
|
|
|
+ guard self.isAutoPlay && self.realNumberOfItems() > 1 else { return }
|
|
|
+ self.currentIndex += 1
|
|
|
+ self.scrollToItem(at: self.currentIndex, animated: true)
|
|
|
+ self.perform(#selector(self.bannerScrollNext), with: nil, afterDelay: self.autoTimeInterval)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+public extension MVBanner {
|
|
|
+ final func register<T: UICollectionViewCell>(classCellType: T.Type) {
|
|
|
+ let cellID = String(describing: classCellType)
|
|
|
+ self.collectionView.register(classCellType, forCellWithReuseIdentifier: cellID)
|
|
|
+ }
|
|
|
+ final func register<T: UICollectionViewCell>(nibCellType: T.Type) {
|
|
|
+ let cellID = String(describing: nibCellType)
|
|
|
+ let nib = UINib.init(nibName: cellID, bundle: nil)
|
|
|
+ self.collectionView.register(nib, forCellWithReuseIdentifier: cellID)
|
|
|
+ }
|
|
|
+ final func dequeueReusableCell<T: UICollectionViewCell>(for indexPath: IndexPath, cellType: T.Type = T.self) -> T {
|
|
|
+ let cellID = String(describing: cellType)
|
|
|
+ let bareCell = self.collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath)
|
|
|
+ guard let cell = bareCell as? T else {
|
|
|
+ fatalError(
|
|
|
+ "Failed to dequeue a cell with identifier \(cellID) matching type \(cellType.self). "
|
|
|
+ + "Check that the reuseIdentifier is set properly in your XIB/Storyboard "
|
|
|
+ + "and that you registered the cell beforehand"
|
|
|
+ )
|
|
|
+ }
|
|
|
+ return cell
|
|
|
+ }
|
|
|
+ final func reloadData() {
|
|
|
+ let count = self.dataSource?.numberOfItems() ?? 0
|
|
|
+ self.collectionView.isUserInteractionEnabled = count > 1
|
|
|
+ self.collectionView.reloadData()
|
|
|
+ self.scrollToItem(realAt: 0, animated: true)
|
|
|
+ }
|
|
|
+ final func scrollToItem(realAt index: Int, animated: Bool) {
|
|
|
+ let indexPath = self.indexPath(realIndex: index)
|
|
|
+ self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated)
|
|
|
+ self.setCurrentPage(index)
|
|
|
+ }
|
|
|
+ final func scrollToItem(at index: Int, animated: Bool) {
|
|
|
+ let indexPath = IndexPath(item: index, section: 0)
|
|
|
+ self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated)
|
|
|
+ let page = self.realIndex(index: index)
|
|
|
+ self.setCurrentPage(page)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension MVBanner: UICollectionViewDataSource, UICollectionViewDelegate {
|
|
|
+ // MARK: - UICollectionViewDataSource
|
|
|
+ public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
|
+ return self.numberOfItems()
|
|
|
+ }
|
|
|
+ public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
|
+ let realIndexPath = self.realIndexPath(index: indexPath.item)
|
|
|
+ return self.dataSource?.banner(self, cellForItemAt: realIndexPath) ?? UICollectionViewCell()
|
|
|
+ }
|
|
|
+ // MARK: - UICollectionViewDelegate
|
|
|
+ public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
|
+ if (delegate?.responds(to: #selector(delegate?.banner(_:didSelectItemAt:))) ?? false) {
|
|
|
+ let realIndexPath = self.realIndexPath(index: indexPath.item)
|
|
|
+ self.delegate?.banner?(self, didSelectItemAt: realIndexPath)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension MVBanner: UIScrollViewDelegate {
|
|
|
+ // MARK: - UIScrollViewDelegate
|
|
|
+ public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
|
+ scrollView.isPagingEnabled = true
|
|
|
+ self.bannerStop()
|
|
|
+ }
|
|
|
+ public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
+ }
|
|
|
+ public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
|
+ if(velocity.x > 0) {
|
|
|
+ self.currentIndex += 1
|
|
|
+ }
|
|
|
+ else if (velocity.x < 0) {
|
|
|
+ self.currentIndex -= 1
|
|
|
+ }
|
|
|
+ else if (velocity.x == 0) {
|
|
|
+ let centerX = scrollView.contentOffset.x + scrollView.frame.size.width * 0.5
|
|
|
+ var minDistance: CGFloat = CGFloat.greatestFiniteMagnitude
|
|
|
+ let indexPaths = self.collectionView.indexPathsForVisibleItems
|
|
|
+ for indexPath in indexPaths {
|
|
|
+ let attributes = self.collectionView.layoutAttributesForItem(at: indexPath)
|
|
|
+ if let attri = attributes {
|
|
|
+ let distance = abs(attri.center.x - centerX)
|
|
|
+ if (abs(minDistance) > abs(distance)) {
|
|
|
+ minDistance = distance
|
|
|
+ self.currentIndex = attri.indexPath.row
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
|
|
+ scrollView.isPagingEnabled = false
|
|
|
+ self.scrollToItem(at: self.currentIndex, animated: true)
|
|
|
+ }
|
|
|
+ public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
|
|
+ scrollView.isPagingEnabled = false
|
|
|
+ self.checkRealOutOfBounds()
|
|
|
+ self.bannerPlay()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|