- 更新模块文件数(EPUBCore 31+2, EPUBTextRendering 13, ReaderView 5, EPUBUI 19) - 更新 Swift 版本(5.10)、iOS 版本(15.0)、行数等基本信息 - 补充新增文件文档(TextAnchor, RenderRequest, WebViewDebug, AssetRepository, TextIndexTable, TextPerformanceSampler, ChapterData, PageInteractionController 等) - 更新 RDEPUBLocation 模型(新增 rangeAnchor 字段) - 更新 RDEPUBReaderConfiguration(13 项配置)、Delegate 签名、Persistence(8 方法) - 修正 RDURLReaderController 归属(ReaderView → EPUBUI) - 移除过时的 LegacyRDReaderController 引用 - 标记 SS→RD 命名迁移为已完成
11 KiB
11 KiB
RDReaderView 功能实现逻辑
1. 范围与目标
- 代码范围:
Sources/RDReaderView/ReaderView/(5 个 Swift 文件) - 目标:说明分页阅读器容器如何管理四种显示模式、DataSource/Delegate 协议、翻页交互、工具栏动画、双屏适配和 RTL 支持。
- 主链路关键词:
RDReaderDataSource -> reloadData -> DisplayType 切换 -> 翻页/滚动 -> RDReaderDelegate.pageNum -> 工具栏显隐。
2. 关键对象职责
2.1 核心容器 RDReaderView
- 文件:
Sources/RDReaderView/ReaderView/RDReaderView.swift(~1219 行) - 入口方法:
reloadData() - 职责:
- 管理四种显示模式的视图层级切换
- 持有
UIPageViewController(pageCurl 模式)或UICollectionView(滚动模式) - 处理点击手势(左/中/右三区域)
- 管理工具栏(topToolView / bottomToolView)的显示/隐藏动画
- 检测横竖屏变化并触发重新布局
- 管理双屏配对逻辑(含封面页处理)
- RTL 语言方向支持
2.2 自定义布局 RDReaderFlowLayout
- 文件:
Sources/RDReaderView/ReaderView/RDReaderFlowLayout.swift(~375 行) - 职责:
- 继承
UICollectionViewFlowLayout,为三种滚动模式提供布局计算 - 水平滚动:全屏宽 item,pagingEnabled
- 垂直滚动:可变高度 item,累加计算
- 水平覆盖滚动:zIndex 分层 + 阴影效果模拟深度
- 封面感知帧计算:封面页全屏宽,后续页面两两配对半屏宽
- 继承
2.3 内容 Cell RDReaderContentCell
- 文件:
Sources/RDReaderView/ReaderView/RDReaderContentCell.swift(~55 行) - 职责:
UICollectionViewCell子类,作为内容视图的薄壳宿主containerView属性 setter 自动移除旧视图、添加新视图layoutSubviews将 containerView 填满 contentView.bounds
2.4 页面子控制器 RDReaderPageChildViewController
- 文件:
Sources/RDReaderView/ReaderView/RDReaderPageChildViewController.swift(~89 行) - 职责:
- 仅用于 pageCurl 模式,作为
UIPageViewController的子控制器 - 持有
contentView: UIView?和pageNum: Int contentViewdidSet 在 view 已加载时自动调用installContentView()
- 仅用于 pageCurl 模式,作为
2.5 手势控制器 RDReaderGestureController
- 文件:
Sources/RDReaderView/ReaderView/RDReaderGestureController.swift(~58 行) - 职责:
- 当前为占位组件,存储 topToolView / bottomToolView 引用
- 实际手势逻辑在
RDReaderView.tapCenter()中实现
3. 主流程(代码级)
3.1 协议定义
RDReaderDataSource(数据供给):
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(事件回调):
func pageNum(readerView: RDReaderView, pageNum: Int)
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
3.2 初始化与数据加载
- 调用方创建
RDReaderView,设置dataSource和delegate。 - 调用
reloadData()。 reloadData内部调用switchReaderDisplayType(currentDisplayType)重建视图层级。- 同时从
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 模式:
- 同一 collectionView,scrollDirection = .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+2,coverIndex+3 与 coverIndex+4...)
- 无封面页:标准偶奇配对(0+1, 2+3, 4+5...)
空白哨兵页:
blankPageNum = Int.max,blankEndPageNum = Int.max - 1- 仅用于 pageCurl 双页模式,填充封面页独占或总页数为奇数时的右侧空白
3.6 横竖屏变化处理
layoutSubviews()检测previousIsLandscape与当前isLandscape的变化- 异步调用
orientationChanged(isNowLandscape:)避免嵌套布局 - 通知 delegate
readerViewOrientationWillChange - 更新
layout.isLandscapeDualPage和layout.coverPageIndex - pageCurl 模式:重建 UIPageViewController(spineLocation 不可变)并跳转到保存的页面
- 滚动模式:invalidate layout,reload 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 显示模式
RDReaderView.DisplayType
├── .pageCurl // UIPageViewController 翻页效果
├── .horizontalScroll // UICollectionView 水平滚动
├── .verticalScroll // UICollectionView 垂直滚动
└── .horizontalCoverScroll // UICollectionView 水平覆盖动画
5.2 翻页方向
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;否则 1isLandscape: Bool:bounds.width > bounds.heighthasCoverPage: Bool:coverPageIndex != nil
6. 视图绑定规则
- 规则 1:内容视图通过
register(contentView:contentViewWithReuseIdentifier:)注册,内部注册对应的RDReaderContentCell - 规则 2:Cell 复用两层:
RDReaderContentCell(UICollectionViewCell)宿主containerView(实际内容视图) - 规则 3:
pageContentView(readerView:pageNum:containerView:)的containerView参数是从回收 cell 中取出的旧视图,允许数据源复用或重新渲染 - 规则 4:工具栏通过
topToolView/bottomToolView协议方法提供,由RDReaderView管理显隐动画 - 规则 5:RTL 支持通过 collectionView
scaleX: -1+ cell contentViewscaleX: -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 模式需重建 UIPageViewController(spineLocation 不可变)
- 检查
dualPagePair配对逻辑和空白哨兵页处理
- 确认
- 排查 4:RTL 方向错误
- 确认
pageDirection是否设为.rightToLeft - 水平滚动模式检查 collectionView 是否做了
scaleX: -1 - pageCurl 模式检查导航方向是否翻转
- 确认
- 排查 5:工具栏动画异常
- 确认
topToolView/bottomToolView是否通过 dataSource 正确提供 - 检查
toolViewAnimationDuration值 - 确认工具栏显示期间用户交互是否被正确禁用/恢复
- 确认