ReadViewSDK/Doc/RDReaderView_功能实现逻辑.md

253 lines
12 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.

# RDReaderView 功能实现逻辑
## 1. 范围与目标
- 代码范围:`Sources/RDReaderView/` 根目录6 个 Swift 文件)
- 目标说明分页阅读器容器如何管理四种显示模式、DataSource/Delegate 协议、翻页交互、工具栏动画、双屏适配和 RTL 支持。
- 主链路关键词:`RDReaderDataSource -> reloadData -> DisplayType 切换 -> 翻页/滚动 -> RDReaderDelegate.pageNum -> 工具栏显隐`。
## 2. 关键对象职责
### 2.1 核心容器 `RDReaderView`
- 文件:`Sources/RDReaderView/RDReaderView.swift`~717 行)
- 入口方法:`reloadData()`
- 职责:
- 管理四种显示模式的视图层级切换
- 持有 `UIPageViewController`pageCurl 模式)或 `UICollectionView`(滚动模式)
- 处理点击手势(左/中/右三区域)
- 管理工具栏topToolView / bottomToolView的显示/隐藏动画
- 检测横竖屏变化并触发重新布局
- 管理双屏配对逻辑(含封面页处理)
- RTL 语言方向支持
### 2.2 自定义布局 `RDReaderFlowLayout`
- 文件:`Sources/RDReaderView/RDReaderFlowLayout.swift`~387 行)
- 职责:
- 继承 `UICollectionViewFlowLayout`,为三种滚动模式提供布局计算
- 水平滚动:全屏宽 itempagingEnabled
- 垂直滚动:可变高度 item累加计算
- 水平覆盖滚动zIndex 分层 + 阴影效果模拟深度
- 封面感知帧计算:封面页全屏宽,后续页面两两配对半屏宽
### 2.3 内容 Cell `RDReaderContentCell`
- 文件:`Sources/RDReaderView/RDReaderContentCell.swift`~37 行)
- 职责:
- `UICollectionViewCell` 子类,作为内容视图的薄壳宿主
- `containerView` 属性 setter 自动移除旧视图、添加新视图
- `layoutSubviews` 将 containerView 填满 contentView.bounds
### 2.4 页面子控制器 `RDReaderPageChildViewController`
- 文件:`Sources/RDReaderView/RDReaderPageChildViewController.swift`~65 行)
- 职责:
- 仅用于 pageCurl 模式,作为 `UIPageViewController` 的子控制器
- 持有 `contentView: UIView?``pageNum: Int`
- `contentView` didSet 在 view 已加载时自动调用 `installContentView()`
### 2.5 手势控制器 `RDReaderGestureController`
- 文件:`Sources/RDReaderView/RDReaderGestureController.swift`~42 行)
- 职责:
- 当前为占位组件,存储 topToolView / bottomToolView 引用
- 实际手势逻辑在 `RDReaderView.tapCenter()` 中实现
### 2.6 URL 入口 `RDURLReaderController`
- 文件:`Sources/RDReaderView/RDURLReaderController.swift`~105 行)
- 职责:
- 最简入口:传入 URL 即可打开书籍
- `.epub` 扩展名 → 创建 `RDEPUBReaderController(epubURL:configuration:)`
- 其他扩展名 → 使用 `RDPlainTextBookBuilder` 构建 `RDEPUBTextBook`,然后创建 `RDEPUBReaderController(textBook:bookIdentifier:title:textFileURL:configuration:)`
- 分页失败时回退到纯 `UITextView` 展示(尝试 UTF-8 → GBK → GB2312 解码)
- 导航栏标题设为文件名(去掉扩展名)
## 3. 主流程(代码级)
### 3.1 协议定义
**RDReaderDataSource**(数据供给):
```swift
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?
```
**RDReaderDelegate**(事件回调):
```swift
func pageNum(readerView: RDReaderView, pageNum: Int)
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
```
### 3.2 初始化与数据加载
1. 调用方创建 `RDReaderView`,设置 `dataSource``delegate`
2. 调用 `reloadData()`
3. `reloadData` 内部调用 `switchReaderDisplayType(currentDisplayType)` 重建视图层级。
4. 同时从 `dataSource` 获取 `topToolView``bottomToolView` 并添加到视图层级。
### 3.3 四种显示模式切换
**pageCurl 模式**
- 创建 `UIPageViewController`transitionStyle: .pageCurl
- 通过 `attachPageViewControllerIfNeeded()` 添加为父 VC 的 child VC
- 横屏双页时重建 `UIPageViewController`spineLocation: .mid, isDoubleSided: true
- RTL 时翻转导航方向
**horizontalScroll 模式**
- 移除 pageViewController插入 `UICollectionView`
- `RDReaderFlowLayout` 设置 scrollDirection = .horizontal, isPagingEnabled = true
- itemSize 宽度 = 容器宽度 / pagesPerScreen
- RTL 时对 collectionView 做 `scaleX: -1` 翻转cell contentView 再翻转回来
**verticalScroll 模式**
- 同一 collectionViewscrollDirection = .vertical, isPagingEnabled = false
- 页面高度可变,通过 `RDReaderFlowLayoutDataSoure.heigtOfVerticalScrollPage` 查询
- collectionViewContentSize 为所有页面高度之和
**horizontalCoverScroll 模式**
- 水平分页,但带封面滑动动画
- `layoutAttributesForElements` 中:
- 仅计算当前页附近的窄窗口内的 item attributes
- 当前页之前的 item zIndex = -1当前及之后 zIndex = 1
- 顶层页面边缘添加阴影效果
### 3.4 翻页交互
**点击手势**`tapAction(tap:)`
- 屏幕分为左 1/3、中 1/3、右 1/3 三个区域
- 左区域工具栏隐藏时翻上一页RTL 时翻下一页);工具栏显示时触发 `tapCenter()`
- 中区域:始终触发 `tapCenter()`
- 右区域工具栏隐藏时翻下一页RTL 时翻上一页);工具栏显示时触发 `tapCenter()`
**goNextPage() / goPreviousPage()**
- 双页模式使用 `adjacentDualPage(from:forward:)` 计算目标页
- 单页模式直接 +1 / -1
- 调用 `transitionToPage(pageNum:animated:true)` 执行跳转
**工具栏切换**`tapCenter()`
- 显示时topToolView 从上方滑入bottomToolView 从下方滑入CGAffineTransform translationY
- 隐藏时:反向动画
- 工具栏显示期间禁用 collectionView 和 pageViewController 的用户交互
### 3.5 双页配对逻辑
**`dualPagePair(for pageNum:)`** 返回 `(left: Int, right: Int?)`
- 有封面页:封面页 → (coverIndex, nil)封面后的页面两两配对coverIndex+1 与 coverIndex+2coverIndex+3 与 coverIndex+4...
- 无封面页标准偶奇配对0+1, 2+3, 4+5...
**空白哨兵页**
- `blankPageNum = Int.max``blankEndPageNum = Int.max - 1`
- 仅用于 pageCurl 双页模式,填充封面页独占或总页数为奇数时的右侧空白
### 3.6 横竖屏变化处理
1. `layoutSubviews()` 检测 `previousIsLandscape` 与当前 `isLandscape` 的变化
2. 异步调用 `orientationChanged(isNowLandscape:)` 避免嵌套布局
3. 通知 delegate `readerViewOrientationWillChange`
4. 更新 `layout.isLandscapeDualPage``layout.coverPageIndex`
5. pageCurl 模式:重建 UIPageViewControllerspineLocation 不可变)并跳转到保存的页面
6. 滚动模式invalidate layoutreload data强制布局无动画滚动到保存的 offset
### 3.7 CollectionView DataSource / Layout Delegate
**cellForItemAt**
-`dataSource.pageIdentifier` 获取重用标识符
- 出队 `RDReaderContentCell`
- 调用 `dataSource.pageContentView(pageNum:containerView:)` 传入 cell 已有的 containerView 以便复用
- RTL 水平模式对 cell.contentView 做 `scaleX: -1` 翻转
**pageNum(flowLayout:pageIndex:)**
- 布局检测到滚动中页面变化时更新 `currentPage`
## 4. 异常与边界处理
- `currentPage` 初始值为 -1首次设置时 delegate 会收到回调
- 空白哨兵页Int.max / Int.max-1在 delegate 回调中被过滤,不会通知到外部
- pageCurl 模式下 collectionView 的 DataSource 方法存在但不被调用collectionView 已从视图层级移除)
- 横竖屏变化时异步处理避免嵌套 layoutSubviews
- `UIView.ss_superViewController` 通过响应链查找最近的父 VC用于正确挂载 pageViewController
## 5. 数据结构与字段映射
### 5.1 显示模式
```swift
RDReaderView.DisplayType
├── .pageCurl // UIPageViewController 翻页效果
├── .horizontalScroll // UICollectionView 水平滚动
├── .verticalScroll // UICollectionView 垂直滚动
└── .horizontalCoverScroll // UICollectionView 水平覆盖动画
```
### 5.2 翻页方向
```swift
RDReaderView.PageDirection
├── .leftToRight // 默认,中文/英文
└── .rightToLeft // 日文漫画等
```
### 5.3 关键公开属性
| 属性 | 类型 | 默认值 | 用途 |
|------|------|--------|------|
| `dataSource` | `RDReaderDataSource?` | nil | 弱引用数据源 |
| `delegate` | `RDReaderDelegate?` | nil | 弱引用事件监听 |
| `currentDisplayType` | `DisplayType` | .pageCurl | 当前显示模式 |
| `toolViewAnimationDuration` | `TimeInterval` | 0.3 | 工具栏动画时长 |
| `landscapeDualPageEnabled` | `Bool` | false | 横屏双页 |
| `pageDirection` | `PageDirection` | .leftToRight | 翻页方向 |
| `coverPageIndex` | `Int?` | nil | 封面页索引 |
| `currentPage` | `Int` | -1 | 当前页码didSet 通知 delegate |
### 5.4 计算属性
- `pagesPerScreen: Int`:横屏 + 双页启用 + 非垂直滚动 → 2否则 1
- `isLandscape: Bool``bounds.width > bounds.height`
- `hasCoverPage: Bool``coverPageIndex != nil`
## 6. 视图绑定规则
- 规则 1内容视图通过 `register(contentView:contentViewWithReuseIdentifier:)` 注册,内部注册对应的 `RDReaderContentCell`
- 规则 2Cell 复用两层:`RDReaderContentCell`UICollectionViewCell宿主 `containerView`(实际内容视图)
- 规则 3`pageContentView(readerView:pageNum:containerView:)` 的 `containerView` 参数是从回收 cell 中取出的旧视图,允许数据源复用或重新渲染
- 规则 4工具栏通过 `topToolView` / `bottomToolView` 协议方法提供,由 `RDReaderView` 管理显隐动画
- 规则 5RTL 支持通过 collectionView `scaleX: -1` + cell contentView `scaleX: -1` 实现,文本正确显示
## 7. 通知协作与回调机制
- 无 NotificationCenter 使用
- 所有通信通过协议和闭包:
- `RDReaderDataSource`:数据供给(页数、内容视图、标识符、工具栏)
- `RDReaderDelegate`:事件通知(页面变化、横竖屏即将变化)
- `RDReaderFlowLayoutDelegate`:布局检测到页面变化时通知
- `RDReaderFlowLayoutDataSoure`:垂直滚动时提供每页高度
## 8. 联调与排查建议
- 排查 1页面不显示
- 确认 `dataSource` 已设置且 `reloadData()` 已调用
- 确认 `pageCountOfReaderView` 返回值 > 0
- 确认 `pageContentView(pageNum:containerView:)` 返回非 nil 视图
- 排查 2翻页无反应
- 确认工具栏是否正在显示(工具栏显示时左右点击变为切换工具栏)
- 确认 `currentPage` 的 didSet 是否被触发
- pageCurl 模式检查 `UIPageViewController` 的 dataSource 方法是否正确返回
- 排查 3横屏双页异常
- 确认 `landscapeDualPageEnabled` 是否为 true
- pageCurl 模式需重建 UIPageViewControllerspineLocation 不可变)
- 检查 `dualPagePair` 配对逻辑和空白哨兵页处理
- 排查 4RTL 方向错误
- 确认 `pageDirection` 是否设为 `.rightToLeft`
- 水平滚动模式检查 collectionView 是否做了 `scaleX: -1`
- pageCurl 模式检查导航方向是否翻转
- 排查 5工具栏动画异常
- 确认 `topToolView` / `bottomToolView` 是否通过 dataSource 正确提供
- 检查 `toolViewAnimationDuration`
- 确认工具栏显示期间用户交互是否被正确禁用/恢复