- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
456 lines
16 KiB
Markdown
456 lines
16 KiB
Markdown
# 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. 内容 Cell:RDReaderContentCell
|
||
|
||
**文件:** `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:)` — 提供前一页 VC(RTL 时逻辑反转)
|
||
- `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() → 预加载周围页面
|
||
```
|