import UIKit public protocol RDEpubReaderFlowLayoutDataSoure: NSObjectProtocol { func heigtOfVerticalScrollPage(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) -> CGFloat? } @objc public protocol RDEpubReaderFlowLayoutDelegate: NSObjectProtocol { func pageNum(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) } public class RDEpubReaderFlowLayout: UICollectionViewFlowLayout { var displayType: RDEpubReaderView.DisplayType = .horizontalScroll { didSet { invalidateLayout() } } var isLandscapeDualPage: Bool = false { didSet { if oldValue != isLandscapeDualPage { invalidateLayout() } } } var coverPageIndex: Int? = nil { didSet { if oldValue != coverPageIndex { invalidateLayout() } } } private var lastPreparedBoundsSize: CGSize = .zero var pagesPerScreen: Int { guard isLandscapeDualPage else { return 1 } switch displayType { case .horizontalScroll: if let cv = collectionView, cv.bounds.width > cv.bounds.height { return 2 } return 1 default: return 1 } } weak var dataSource: RDEpubReaderFlowLayoutDataSoure? = nil weak var delegate: RDEpubReaderFlowLayoutDelegate? = nil private var hasCoverPageInDualMode: Bool { return pagesPerScreen > 1 && coverPageIndex != nil } private func coverAwareFrame(for index: Int, screenWidth: CGFloat, halfWidth: CGFloat, height: CGFloat) -> CGRect { guard let coverIndex = coverPageIndex else { let pairIdx = index / 2 let side = index % 2 let x = CGFloat(pairIdx) * screenWidth + CGFloat(side) * halfWidth return CGRect(x: x, y: 0, width: halfWidth, height: height) } if index == coverIndex { let screensBeforeCover = coverIndex / 2 let x = CGFloat(screensBeforeCover) * screenWidth return CGRect(x: x, y: 0, width: screenWidth, height: height) } let adjustedIndex = index - (coverIndex + 1) let pairIdx = adjustedIndex / 2 let side = adjustedIndex % 2 let coverScreens = coverIndex / 2 + 1 let x = CGFloat(coverScreens + pairIdx) * screenWidth + CGFloat(side) * halfWidth return CGRect(x: x, y: 0, width: halfWidth, height: height) } private func coverAwareStartIndex(for offset: CGFloat, screenWidth: CGFloat) -> Int { let pps = pagesPerScreen guard let coverIdx = coverPageIndex, hasCoverPageInDualMode else { let screenIndex = Int((offset / screenWidth).rounded(.down)) return screenIndex * pps } let coverScreens = coverIdx / 2 + 1 let coverEnd = CGFloat(coverScreens) * screenWidth if offset < coverEnd { let screenIdx = Int((offset / screenWidth).rounded(.down)) return screenIdx >= coverIdx / 2 ? coverIdx : screenIdx * pps } else { let pairIdx = Int(((offset - coverEnd) / screenWidth).rounded(.down)) return coverIdx + 1 + pairIdx * 2 } } private var currentPage: Int = 0 { didSet { if let delegate = delegate, delegate.responds(to: #selector(RDEpubReaderFlowLayoutDelegate.pageNum(flowLayout:pageIndex:))), currentPage != oldValue, currentPage >= 0 { delegate.pageNum(flowLayout: self, pageIndex: currentPage) } } } init(displayType: RDEpubReaderView.DisplayType) { self.displayType = displayType super.init() } public override func prepare() { super.prepare() guard let collectionView = self.collectionView else { return } guard collectionView.frame.width > 0, collectionView.frame.height > 0 else { return } lastPreparedBoundsSize = collectionView.bounds.size if #available(iOS 11.0, *) { collectionView.contentInsetAdjustmentBehavior = .never } else { collectionView.ss_superViewController?.automaticallyAdjustsScrollViewInsets = false } collectionView.showsVerticalScrollIndicator = false collectionView.showsHorizontalScrollIndicator = false minimumLineSpacing = 0 minimumInteritemSpacing = 0 switch self.displayType { case .horizontalScroll: scrollDirection = .horizontal let columns = CGFloat(pagesPerScreen) itemSize = CGSize(width: collectionView.frame.width / columns, height: collectionView.frame.height) collectionView.isPagingEnabled = true case .verticalScroll: itemSize = CGSize(width: collectionView.frame.width, height: collectionView.frame.height) scrollDirection = .vertical collectionView.isPagingEnabled = false collectionView.showsVerticalScrollIndicator = true default: break } } public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { if newBounds.size != lastPreparedBoundsSize { return true } switch displayType { case .verticalScroll: return true case .horizontalScroll: return true default: return false } } public override var collectionViewContentSize: CGSize { var size = super.collectionViewContentSize guard let collectionView = collectionView else { return size } if displayType == .verticalScroll { let totalHeight = [Int](0.. [UICollectionViewLayoutAttributes]? { var attributes = super.layoutAttributesForElements(in: rect) guard let collectionView = collectionView else { return attributes } if self.displayType == .verticalScroll { let rowCount = collectionView.numberOfItems(inSection: 0) var totalHeight: CGFloat = 0 for index in 0.. collectionView.contentOffset.y { self.currentPage = index break } } (attributes ?? []).forEach({ attr in let indexPath = attr.indexPath let lastRow = max(0, indexPath.row - 1) let height = dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: indexPath.row) ?? collectionView.frame.height var lastTotalHeight: CGFloat = 0 if indexPath.row > 0 { lastTotalHeight = [Int](0...lastRow).reduce(0.0) { result, row in result + (dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: row) ?? collectionView.frame.height) } } if let cell = collectionView.cellForItem(at: indexPath) { cell.layer.shadowOpacity = 0 } attr.frame = CGRect(x: 0, y: lastTotalHeight, width: collectionView.frame.width, height: height) }) } if self.displayType == .horizontalScroll { let pps = pagesPerScreen if hasCoverPageInDualMode { let screenWidth = collectionView.frame.width let halfWidth = screenWidth / 2.0 let rows = collectionView.numberOfItems(inSection: 0) self.currentPage = coverAwareStartIndex(for: collectionView.contentOffset.x, screenWidth: screenWidth) var attrs = [UICollectionViewLayoutAttributes]() for index in 0.. 1 { let currentPage = Int((collectionView.contentOffset.x / collectionView.frame.width).rounded(.down)) * pps self.currentPage = currentPage (attributes ?? []).forEach({ attr in if let cell = collectionView.cellForItem(at: attr.indexPath) { cell.layer.shadowOpacity = 0 } }) } else { let currentPage = Int((collectionView.contentOffset.x / collectionView.frame.width).rounded(.down)) self.currentPage = currentPage (attributes ?? []).forEach({ attr in if let cell = collectionView.cellForItem(at: attr.indexPath) { cell.layer.shadowOpacity = 0 } }) } } return attributes } func currentContentOffset(count: Int) -> CGPoint { guard let collectionView = collectionView else { return .zero } guard collectionView.frame.width > 0, collectionView.frame.height > 0 else { return .zero } let safeCount = max(0, count) switch displayType { case .verticalScroll: guard safeCount > 0 else { return .zero } let totalHeight = [Int](0...(safeCount - 1)).reduce(0.0) { result, pageNum in result + (dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: pageNum) ?? collectionView.frame.height) } return CGPoint(x: 0, y: totalHeight) default: let pps = pagesPerScreen let screenWidth = collectionView.frame.width if hasCoverPageInDualMode, let coverIdx = coverPageIndex { if safeCount == coverIdx { let screensBeforeCover = coverIdx / 2 return CGPoint(x: CGFloat(screensBeforeCover) * screenWidth, y: 0) } if safeCount < coverIdx { return CGPoint(x: CGFloat(safeCount / 2) * screenWidth, y: 0) } let coverScreens = coverIdx / 2 + 1 let adjustedIndex = safeCount - (coverIdx + 1) let pairIndex = adjustedIndex / 2 return CGPoint(x: CGFloat(coverScreens + pairIndex) * screenWidth, y: 0) } else if pps > 1 { let screenIndex = safeCount / pps return CGPoint(x: CGFloat(screenIndex) * screenWidth, y: 0) } else { return CGPoint(x: CGFloat(safeCount) * itemSize.width, y: 0) } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }