ReadViewSDK/Doc/RDReaderView_功能实现逻辑.md
2026-05-21 19:40:51 +08:00

11 KiB
Raw Blame History

RDReaderView 功能实现逻辑

1. 范围与目标

  • 代码范围:Sources/RDReaderView/ 根目录6 个 Swift 文件)
  • 目标说明分页阅读器容器如何管理四种显示模式、DataSource/Delegate 协议、翻页交互、工具栏动画、双屏适配和 RTL 支持。
  • 主链路关键词:RDReaderDataSource -> reloadData -> DisplayType 切换 -> 翻页/滚动 -> RDReaderDelegate.pageNum -> 工具栏显隐

2. 关键对象职责

2.1 核心容器 RDReaderView

  • 文件:Sources/RDReaderView/RDReaderView.swift~717 行)
  • 入口方法:reloadData()
  • 职责:
    • 管理四种显示模式的视图层级切换
    • 持有 UIPageViewControllerpageCurl 模式)或 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
    • 其他 → 创建 RDPlainTextReaderControllerUITextView 只读展示,尝试 UTF-8 → GBK → GB2312 解码)
    • 导航栏标题设为文件名(去掉扩展名)

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 初始化与数据加载

  1. 调用方创建 RDReaderView,设置 dataSourcedelegate
  2. 调用 reloadData()
  3. reloadData 内部调用 switchReaderDisplayType(currentDisplayType) 重建视图层级。
  4. 同时从 dataSource 获取 topToolViewbottomToolView 并添加到视图层级。

3.3 四种显示模式切换

pageCurl 模式

  • 创建 UIPageViewControllertransitionStyle: .pageCurl
  • 通过 attachPageViewControllerIfNeeded() 添加为父 VC 的 child VC
  • 横屏双页时重建 UIPageViewControllerspineLocation: .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.maxblankEndPageNum = Int.max - 1
  • 仅用于 pageCurl 双页模式,填充封面页独占或总页数为奇数时的右侧空白

3.6 横竖屏变化处理

  1. layoutSubviews() 检测 previousIsLandscape 与当前 isLandscape 的变化
  2. 异步调用 orientationChanged(isNowLandscape:) 避免嵌套布局
  3. 通知 delegate readerViewOrientationWillChange
  4. 更新 layout.isLandscapeDualPagelayout.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 显示模式

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否则 1
  • isLandscape: Boolbounds.width > bounds.height
  • hasCoverPage: BoolcoverPageIndex != nil

6. 视图绑定规则

  • 规则 1内容视图通过 register(contentView:contentViewWithReuseIdentifier:) 注册,内部注册对应的 RDReaderContentCell
  • 规则 2Cell 复用两层:RDReaderContentCellUICollectionViewCell宿主 containerView(实际内容视图)
  • 规则 3pageContentView(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
    • 确认工具栏显示期间用户交互是否被正确禁用/恢复