ReadViewSDK/Doc/ReaderView_CODE_REFERENCE.md
shenlei c65c190b71 feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
2026-06-22 20:26:34 +08:00

456 lines
16 KiB
Markdown
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.

# ReaderView 模块代码级参考文档
> 最后更新2026-06-18
---
## 1. 模块概述
`ReaderView` 是 ReadViewSDK 的**通用翻页容器层**,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 `RDReaderPageProvider` 协议提供页面内容视图ReaderView 负责容器管理和翻页调度。
**文件清单14 个 Swift 文件):**
| 文件 | 核心类型 | 职责 |
|------|----------|------|
| `RDReaderView.swift` | `RDReaderView` | 主容器视图,协调所有子组件 |
| `RDReaderViewProtocols.swift` | 协议 + 枚举 | 数据源、代理、导航协议定义 |
| `RDReaderFlowLayout.swift` | `RDReaderFlowLayout` | UICollectionView 自定义布局 |
| `RDReaderGestureController.swift` | `RDReaderGestureController` | 手势控制器(预留) |
| `RDReaderContentCell.swift` | `RDReaderContentCell` | CollectionView 内容 Cell |
| `RDReaderPageChildViewController.swift` | `RDReaderPageChildViewController` | PageCurl 模式子 VC |
| `RDReaderView+PageCurl.swift` | Extension | UIPageViewController 数据源/代理 |
| `RDReaderView+CollectionView.swift` | Extension | UICollectionView 数据源/布局代理 |
| `RDReaderView+ContentAccess.swift` | Extension | 内容视图访问与复用 |
| `RDReaderView+ToolView.swift` | Extension | 工具栏安装与动画 |
| `Paging/RDReaderPagingController.swift` | `RDReaderPagingController` | 翻页状态机 |
| `Paging/RDReaderPreloadController.swift` | `RDReaderPreloadController` | 页面预加载与缓存 |
| `Paging/RDReaderSpreadResolver.swift` | `RDReaderSpreadResolver` | 双页展开计算 |
| `Paging/RDReaderTapRegionHandler.swift` | `RDReaderTapRegionHandler` | 点击区域判定 |
---
## 2. 协议定义RDReaderViewProtocols.swift
### 2.1 RDReaderDataSource旧版数据源已废弃
```swift
@objc public protocol RDReaderDataSource: NSObjectProtocol {
func pageCountOfReaderView(readerView: RDReaderView) -> Int
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
}
```
> 向后兼容保留,新代码应使用 `RDReaderPageProvider`。
### 2.2 RDReaderPageProvider推荐数据源
```swift
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
func numberOfPages(in readerView: RDReaderView) -> Int
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
}
```
| 方法 | 说明 |
|------|------|
| `numberOfPages(in:)` | 返回总页数 |
| `readerView(_:viewForPageAt:reusableView:)` | 为指定页码提供内容视图,`reusableView` 可复用 |
| `pageIdentifier(in:index:)` | 返回页视图复用标识符,用于 CollectionView 注册 |
| `readerViewTopChrome(_:)` | 返回顶部工具栏视图 |
| `readerViewBottomChrome(_:)` | 返回底部工具栏视图 |
### 2.3 RDReaderDelegate
```swift
@objc public protocol RDReaderDelegate: NSObjectProtocol {
func pageNum(readerView: RDReaderView, pageNum: Int)
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
}
```
### 2.4 RDReaderPageNavigating
```swift
public protocol RDReaderPageNavigating: AnyObject {
var currentPage: Int { get }
func reloadPages()
func transition(to page: Int, animated: Bool)
}
```
`RDReaderView` 遵循此协议,提供统一的页面导航接口。
### 2.5 枚举类型
```swift
extension RDReaderView {
public enum DisplayType {
case pageCurl // 仿真翻页UIPageViewController
case horizontalScroll // 水平滑动UICollectionView
case verticalScroll // 垂直滚动UICollectionView
}
public enum PageDirection {
case leftToRight // LTR
case rightToLeft // RTL
}
}
```
---
## 3. 核心类RDReaderView
**文件:** `RDReaderView.swift`
`RDReaderView` 是一个 `UIView` 子类,作为翻页容器的主入口。内部管理两种翻页引擎:
- **PageCurl 模式**:使用 `UIPageViewController` 实现仿真翻页
- **Scroll 模式**:使用 `UICollectionView` + 自定义 `RDReaderFlowLayout` 实现滑动翻页
### 3.1 关键属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `currentPage` | `Int` | 当前页码,变化时通知 delegate |
| `currentDisplayType` | `DisplayType` | 当前显示模式 |
| `pageDirection` | `PageDirection` | 页面方向LTR/RTL |
| `landscapeDualPageEnabled` | `Bool` | 是否启用横屏双页 |
| `coverPageIndex` | `Int?` | 封面页索引(独占一屏) |
| `pagesPerScreen` | `Int` | 每屏页数(横屏双页时为 2 |
| `preloadRadius` | `Int` | 预加载半径(默认 1 |
| `dataSource` | `RDReaderDataSource?` | 旧版数据源 |
| `pageProvider` | `RDReaderPageProvider?` | 推荐数据源 |
| `delegate` | `RDReaderDelegate?` | 事件代理 |
| `toolViewAnimationDuration` | `TimeInterval` | 工具栏动画时长0.3s |
### 3.2 关键方法
```swift
/// 切换显示模式pageCurl / horizontalScroll / verticalScroll
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType)
/// 跳转到指定页
public func transitionToPage(pageNum: Int, animated: Bool = false)
/// 重新加载所有页面
public func reloadData()
/// 仅重载页数(不重建内容)
public func reloadPageCountOnly()
/// 判断指定页是否为全屏页(封面页独占一屏)
public func isFullScreenPage(_ pageNum: Int) -> Bool
```
### 3.3 内部子组件
| 组件 | 类型 | 职责 |
|------|------|------|
| `pageViewController` | `UIPageViewController` | PageCurl 翻页引擎 |
| `collectionView` | `UICollectionView` | Scroll 翻页引擎 |
| `layout` | `RDReaderFlowLayout` | CollectionView 自定义布局 |
| `spreadResolver` | `RDReaderSpreadResolver` | 双页配对计算 |
| `tapRegionHandler` | `RDReaderTapRegionHandler` | 点击区域判定 |
| `preloadController` | `RDReaderPreloadController` | 页面预加载与缓存 |
| `pagingController` | `RDReaderPagingController` | 翻页状态管理 |
---
## 4. 翻页状态机RDReaderPagingController
**文件:** `Paging/RDReaderPagingController.swift`
管理 PageCurl 模式下的翻页请求队列,防止动画冲突。
```swift
struct RDReaderPagingController {
struct PageTransitionRequest: Equatable {
let pageNum: Int
let animated: Bool
}
var pendingTransitionRequest: PageTransitionRequest? // 待处理请求
var isTransitioning: Bool // 是否正在翻页动画中
var didBuildUI: Bool // UI 是否已构建
/// 判断是否应排队请求PageCurl 模式下动画中返回 true
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDReaderView.DisplayType) -> Bool
/// 完成翻页动画,返回待处理的请求
mutating func finishPageCurlTransition() -> PageTransitionRequest?
/// 重置状态(用于故障恢复)
mutating func resetPendingState()
}
```
---
## 5. 页面预加载RDReaderPreloadController
**文件:** `Paging/RDReaderPreloadController.swift`
负责在当前页周围预渲染页面视图,减少翻页时的白屏时间。
### 5.1 核心机制
- **缓存签名CacheSignature**:基于 `displayType + isLandscape + pagesPerScreen + boundsSize` 生成签名,签名变化时清空缓存
- **双缓存池**`preloadedPageViews`(预加载池)和 `pageCurlCachedViews`PageCurl 缓存池)
- **预测性预加载**:根据 `preferredForward` 方向多预加载一页
### 5.2 关键方法
```swift
/// 为指定页获取视图(优先从缓存取)
func pageViewForDisplay(pageNum: Int, environment: Environment, contentViewProvider: (Int, UIView?) -> UIView?) -> UIView
/// 在指定页周围预加载
func prime(around pageNum: Int, preferredForward: Bool?, parentView: UIView, environment: Environment, contentViewProvider: (Int, UIView?) -> UIView?)
/// 取走预加载的视图(用于 CollectionView 复用)
func takePreloadedView(for pageNum: Int) -> UIView?
/// 使缓存失效
func invalidate(environment: Environment)
```
### 5.3 Environment 结构体
```swift
struct Environment {
let displayType: RDReaderView.DisplayType
let isLandscape: Bool
let pagesPerScreen: Int
let boundsSize: CGSize
let landscapeDualPageEnabled: Bool
let coverPageIndex: Int?
let totalPages: Int
let spreadResolver: RDReaderSpreadResolver
}
```
---
## 6. 双页展开计算RDReaderSpreadResolver
**文件:** `Paging/RDReaderSpreadResolver.swift`
纯函数式结构体,负责双页模式下的页面配对和导航计算。
```swift
struct RDReaderSpreadResolver {
/// 判断是否为全屏页(封面页独占一屏)
func isFullScreenPage(_ pageNum: Int, landscapeDualPageEnabled: Bool, isLandscape: Bool, coverPageIndex: Int?) -> Bool
/// 计算双页配对left, right?right 为 nil 表示独占一屏
func dualPagePair(for pageNum: Int, totalPages: Int, coverPageIndex: Int?) -> (left: Int, right: Int?)
/// 计算相邻双页的起始页码
func adjacentDualPage(from pageNum: Int, totalPages: Int, coverPageIndex: Int?, forward: Bool) -> Int?
/// 计算下一页(支持单页和双页模式)
func nextPage(from currentPage: Int, totalPages: Int, pagesPerScreen: Int, coverPageIndex: Int?, forward: Bool) -> Int?
}
```
**封面页逻辑:**
- 封面页(`coverPageIndex`)独占一屏,不与其他页配对
- 封面后的页面从 `coverIndex + 1` 开始两两配对
---
## 7. 点击区域判定RDReaderTapRegionHandler
**文件:** `Paging/RDReaderTapRegionHandler.swift`
将屏幕三等分,判定点击属于左/中/右区域。
```swift
struct RDReaderTapRegionHandler {
func resolveTapEvent(point: CGPoint, viewFrame: CGRect, isToolViewVisible: Bool) -> RDReaderView.TapEvent
}
```
**逻辑:**
- 左 1/3 → `.left`(上一页),工具栏可见时改为 `.center`
- 中 1/3 → `.center`(切换工具栏)
- 右 1/3 → `.right`(下一页),工具栏可见时改为 `.center`
---
## 8. 流式布局RDReaderFlowLayout
**文件:** `RDReaderFlowLayout.swift`
`UICollectionViewFlowLayout` 子类,支持水平滚动和垂直滚动两种模式。
### 8.1 关键属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `displayType` | `RDReaderView.DisplayType` | 布局模式 |
| `isLandscapeDualPage` | `Bool` | 是否横屏双页 |
| `coverPageIndex` | `Int?` | 封面页索引 |
| `pagesPerScreen` | `Int` | 每屏页数 |
### 8.2 协议
```swift
public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
}
@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
}
```
### 8.3 关键方法
```swift
/// 计算指定页码的 contentOffset
func currentContentOffset(count: Int) -> CGPoint
```
### 8.4 封面页布局逻辑
双页模式下,封面页占满整屏宽度,后续页面两两配对占半屏宽度。`coverAwareFrame(for:screenWidth:halfWidth:height:)` 方法根据页码计算对应的 frame。
---
## 9. 内容 CellRDReaderContentCell
**文件:** `RDReaderContentCell.swift`
`UICollectionViewCell` 子类,用于 Scroll 模式下承载页面内容视图。
```swift
class RDReaderContentCell: UICollectionViewCell {
var containerView: UIView? // 设置时自动添加到 contentView移除旧视图
}
```
---
## 10. PageCurl 子控制器RDReaderPageChildViewController
**文件:** `RDReaderPageChildViewController.swift`
`UIViewController` 子类,作为 `UIPageViewController` 的页面 VC。
```swift
class RDReaderPageChildViewController: UIViewController {
var contentView: UIView? // 内容视图,设置时自动安装到容器
var pageNum: Int // 对应页码
init(contentView: UIView?, pageNum: Int = 0)
}
```
---
## 11. Extension 汇总
### 11.1 RDReaderView+PageCurl
实现 `UIPageViewControllerDataSource``UIPageViewControllerDelegate`
- `pageViewController(_:viewControllerBefore:)` — 提供前一页 VCRTL 时逻辑反转)
- `pageViewController(_:viewControllerAfter:)` — 提供后一页 VC
- `pageViewController(_:didFinishAnimating:...)` — 完成动画后更新 currentPage
- `pageViewController(_:willTransitionTo:)` — 即将翻页时预加载
**特殊页码:**
- `RDReaderView.blankPageNum``Int.max`)— 双页模式下的空白页
- `RDReaderView.blankEndPageNum``Int.max - 1`)— 末尾空白页
### 11.2 RDReaderView+CollectionView
实现 `UICollectionViewDataSource`、`RDReaderFlowLayoutDelegate`、`RDReaderFlowLayoutDataSoure`
- `collectionView(_:cellForItemAt:)` — 复用预加载视图或创建新 Cell
- `collectionView(_:numberOfItemsInSection:)` — 返回总页数
- `pageNum(flowLayout:pageIndex:)` — 滚动时更新当前页码
### 11.3 RDReaderView+ContentAccess
提供内容视图的注册、复用和查询:
```swift
/// 注册内容视图类型(类似 UICollectionView 的 register
public func register(contentView: UIView.Type, contentViewWithReuseIdentifier identifier: String)
/// 获取可复用的内容视图
public func dequeueReusableContentView(withReuseIdentifier identifier: String, for pageNum: Int) -> UIView
/// 获取指定页的内容视图
public func pageContentView(pageNum: Int) -> UIView?
/// 计算单页尺寸(考虑双页模式)
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize
```
### 11.4 RDReaderView+ToolView
管理顶部/底部工具栏的安装、显示/隐藏动画:
```swift
/// 切换工具栏显示状态(点击中心区域触发)
func tapCenter()
/// 安装工具栏视图到指定位置
func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition)
/// 更新工具栏高度约束
func updateToolViewHeightConstraintsIfNeeded()
```
**动画效果:** 顶部工具栏从上方滑入,底部工具栏从下方滑入。
---
## 12. 设计模式总结
| 模式 | 应用 |
|------|------|
| **策略模式** | `DisplayType` 切换 PageCurl / Scroll 两种翻页策略 |
| **适配器模式** | `RDReaderLegacyDataSourceAdapter` 将旧 `RDReaderDataSource` 适配为 `RDReaderPageProvider` |
| **命令队列** | `RDReaderPagingController` 管理翻页请求队列 |
| **缓存签名** | `RDReaderPreloadController.CacheSignature` 检测环境变化自动失效 |
| **关注点分离** | Extension 将不同功能拆分到独立文件 |
| **纯函数** | `RDReaderSpreadResolver``RDReaderTapRegionHandler` 无状态计算 |
---
## 13. 数据流图
```
用户点击屏幕
RDReaderTapRegionHandler.resolveTapEvent()
├── .left → goPreviousPage() ──→ spreadResolver.nextPage(forward: false)
├── .right → goNextPage() ──→ spreadResolver.nextPage(forward: true)
└── .center → tapCenter() ──→ 显示/隐藏工具栏
transitionToPage(pageNum:)
├── PageCurl 模式
│ ├── pagingController.shouldQueuePageTransition() → 排队或执行
│ ├── pageViewForDisplay() → preloadController 取缓存视图
│ ├── pageViewController.setViewControllers()
│ └── primePageCache() → 预加载周围页面
└── Scroll 模式
├── collectionView.reloadData()
├── collectionView.setContentOffset()
└── primePageCache() → 预加载周围页面
```