feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,56 +1,24 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// 自定义流式布局,支持阅读器的三种滚动布局模式:
|
||||
/// - horizontalScroll:水平滚动,每屏1项(竖屏)或2项(横屏双页),支持分页
|
||||
/// - verticalScroll:垂直滚动,全宽项目,支持可变高度
|
||||
///
|
||||
/// 通过 ``displayType`` 属性切换布局模式。
|
||||
/// 通过 ``isLandscapeDualPage`` 和 ``coverPageIndex`` 控制横屏双页和封面页逻辑。
|
||||
public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
/// 布局模式,切换时自动使布局失效并重新计算
|
||||
var displayType: RDReaderView.DisplayType = .horizontalScroll {
|
||||
didSet {
|
||||
invalidateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否启用横屏双页模式
|
||||
/// 启用后水平滚动模式下每屏显示两项(各占半屏宽度)
|
||||
var isLandscapeDualPage: Bool = false {
|
||||
didSet {
|
||||
if oldValue != isLandscapeDualPage {
|
||||
@@ -59,8 +27,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 封面页索引
|
||||
/// 设置后封面页独占整屏,后续页面两两配对各占半屏
|
||||
var coverPageIndex: Int? = nil {
|
||||
didSet {
|
||||
if oldValue != coverPageIndex {
|
||||
@@ -69,11 +35,8 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一次 prepare 时的 bounds 尺寸,用于检测尺寸变化
|
||||
private var lastPreparedBoundsSize: CGSize = .zero
|
||||
|
||||
/// 每屏显示的页数
|
||||
/// 仅在水平滚动+横屏双页模式下返回2,其他情况返回1
|
||||
var pagesPerScreen: Int {
|
||||
guard isLandscapeDualPage else { return 1 }
|
||||
switch displayType {
|
||||
@@ -87,24 +50,14 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 流式布局数据源,提供垂直滚动模式的页面高度
|
||||
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
|
||||
@@ -125,11 +78,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
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 {
|
||||
@@ -147,7 +95,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前可见的页码,值变化时通过 delegate 通知外部
|
||||
private var currentPage: Int = 0 {
|
||||
|
||||
didSet {
|
||||
@@ -157,15 +104,12 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化方法
|
||||
/// - Parameter displayType: 初始布局模式
|
||||
init(displayType: RDReaderView.DisplayType) {
|
||||
self.displayType = displayType
|
||||
super.init()
|
||||
|
||||
}
|
||||
|
||||
/// 布局准备阶段,配置 item 尺寸、滚动方向和分页行为
|
||||
public override func prepare() {
|
||||
super.prepare()
|
||||
guard let collectionView = self.collectionView else {
|
||||
@@ -203,8 +147,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
}
|
||||
|
||||
/// 判断 bounds 变化是否需要使布局失效
|
||||
/// 尺寸变化时总是返回 true,布局模式变化也返回 true
|
||||
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
if newBounds.size != lastPreparedBoundsSize {
|
||||
return true
|
||||
@@ -219,9 +161,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算 collectionView 的总内容尺寸
|
||||
/// 垂直滚动模式:累加所有页面高度
|
||||
/// 封面双页模式:封面占一屏 + 后续页面按两两配对计算屏幕数
|
||||
public override var collectionViewContentSize: CGSize {
|
||||
var size = super.collectionViewContentSize
|
||||
guard let collectionView = collectionView else {
|
||||
@@ -243,9 +182,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
return size
|
||||
}
|
||||
|
||||
/// 计算指定矩形区域内的布局属性
|
||||
/// 垂直滚动模式:根据可变高度定位每个 item
|
||||
/// 水平滚动模式:根据当前偏移量计算当前页码,清除 cell 阴影
|
||||
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
var attributes = super.layoutAttributesForElements(in: rect)
|
||||
guard let collectionView = collectionView else {
|
||||
@@ -279,7 +215,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if self.displayType == .horizontalScroll {
|
||||
let pps = pagesPerScreen
|
||||
|
||||
@@ -325,10 +260,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
return attributes
|
||||
}
|
||||
|
||||
/// 根据页码计算对应的 contentOffset
|
||||
/// 用于跳转到指定页面时设置滚动位置
|
||||
/// - Parameter count: 目标页码
|
||||
/// - Returns: 对应的 contentOffset 坐标
|
||||
func currentContentOffset(count: Int) -> CGPoint {
|
||||
guard let collectionView = collectionView else {
|
||||
return .zero
|
||||
@@ -368,7 +299,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user