ReadViewSDK/Sources/RDReaderView/ReaderView/RDReaderFlowLayout.swift
shen 54798ba578 refactor: 添加中文注释 + 优化模块结构
- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级)
- 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行)
- 根目录翻页容器文件移入 ReaderView/ 目录
- Resources/ 移入 EPUBCore/Resources/(与使用者归属一致)
- RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖)
- RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层)
- 更新 podspec 资源路径
2026-05-25 10:19:14 +08:00

376 lines
15 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// RDReaderFlowLayout.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/6.
//
// UICollectionViewFlowLayout
// item +
//
//
// RDReaderView RDReaderView
//
import UIKit
///
///
public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
///
/// - Parameters:
/// - flowLayout:
/// - pageIndex:
/// - Returns: nil 使collectionView.frame.height
func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
}
///
///
@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
///
/// - Parameters:
/// - flowLayout:
/// - pageIndex:
func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
}
///
/// - horizontalScroll12
/// - verticalScroll
///
/// ``displayType``
/// ``isLandscapeDualPage`` ``coverPageIndex``
public class RDReaderFlowLayout: UICollectionViewFlowLayout {
/// 使
var displayType: RDReaderView.DisplayType = .horizontalScroll {
didSet {
invalidateLayout()
}
}
///
///
var isLandscapeDualPage: Bool = false {
didSet {
if oldValue != isLandscapeDualPage {
invalidateLayout()
}
}
}
///
///
var coverPageIndex: Int? = nil {
didSet {
if oldValue != coverPageIndex {
invalidateLayout()
}
}
}
/// prepare bounds
private var lastPreparedBoundsSize: CGSize = .zero
///
/// +21
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: RDReaderFlowLayoutDataSoure? = nil
///
weak var delegate: RDReaderFlowLayoutDelegate? = nil
///
private var hasCoverPageInDualMode: Bool {
return pagesPerScreen > 1 && coverPageIndex != nil
}
/// item frame
///
/// - Parameters:
/// - index: item
/// - screenWidth:
/// - halfWidth:
/// - height:
/// - Returns: item frame
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)
}
/// item
/// - Parameters:
/// - offset:
/// - screenWidth:
/// - Returns: item
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
}
}
/// delegate
private var currentPage: Int = 0 {
didSet {
if let delegate = delegate, delegate.responds(to: #selector(RDReaderFlowLayoutDelegate.pageNum(flowLayout:pageIndex:))), currentPage != oldValue, currentPage >= 0 {
delegate.pageNum(flowLayout: self, pageIndex: currentPage)
}
}
}
///
/// - Parameter displayType:
init(displayType: RDReaderView.DisplayType) {
self.displayType = displayType
super.init()
}
/// item
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
}
}
/// bounds 使
/// true true
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
}
}
/// collectionView
///
/// +
public override var collectionViewContentSize: CGSize {
var size = super.collectionViewContentSize
guard let collectionView = collectionView else {
return size
}
if displayType == .verticalScroll {
let totalHeight = [Int](0..<collectionView.numberOfItems(inSection: 0)).reduce(0.0, { result, num in
result + (dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: num) ?? collectionView.frame.height)
})
size.height = totalHeight
}
if hasCoverPageInDualMode, displayType == .horizontalScroll {
let totalItems = collectionView.numberOfItems(inSection: 0)
let screenWidth = collectionView.frame.width
let remainingItems = max(0, totalItems - 1)
let pairedScreens = (remainingItems + 1) / 2
size.width = screenWidth * CGFloat(1 + pairedScreens)
}
return size
}
///
/// item
/// cell
public override func layoutAttributesForElements(in rect: CGRect) -> [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..<rowCount {
totalHeight += dataSource?.heigtOfVerticalScrollPage(flowLayout: self, pageIndex: index) ?? collectionView.frame.height
if totalHeight > 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..<rows {
let frame = coverAwareFrame(for: index, screenWidth: screenWidth, halfWidth: halfWidth, height: collectionView.frame.height)
guard frame.intersects(rect) else { continue }
let indexPath = IndexPath(item: index, section: 0)
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attr.frame = frame
attrs.append(attr)
if let cell = collectionView.cellForItem(at: indexPath) {
cell.layer.shadowOpacity = 0
}
}
attributes = attrs
} else if pps > 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
}
/// contentOffset
///
/// - Parameter count:
/// - Returns: contentOffset
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")
}
}