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

16 KiB
Raw Blame History

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旧版数据源已废弃

@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推荐数据源

@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

@objc public protocol RDReaderDelegate: NSObjectProtocol {
    func pageNum(readerView: RDReaderView, pageNum: Int)
    @objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
}

2.4 RDReaderPageNavigating

public protocol RDReaderPageNavigating: AnyObject {
    var currentPage: Int { get }
    func reloadPages()
    func transition(to page: Int, animated: Bool)
}

RDReaderView 遵循此协议,提供统一的页面导航接口。

2.5 枚举类型

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 关键方法

/// 切换显示模式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 模式下的翻页请求队列,防止动画冲突。

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(预加载池)和 pageCurlCachedViewsPageCurl 缓存池)
  • 预测性预加载:根据 preferredForward 方向多预加载一页

5.2 关键方法

/// 为指定页获取视图(优先从缓存取)
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 结构体

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

纯函数式结构体,负责双页模式下的页面配对和导航计算。

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

将屏幕三等分,判定点击属于左/中/右区域。

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 协议

public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
    func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
}

@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
    func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
}

8.3 关键方法

/// 计算指定页码的 contentOffset
func currentContentOffset(count: Int) -> CGPoint

8.4 封面页布局逻辑

双页模式下,封面页占满整屏宽度,后续页面两两配对占半屏宽度。coverAwareFrame(for:screenWidth:halfWidth:height:) 方法根据页码计算对应的 frame。


9. 内容 CellRDReaderContentCell

文件: RDReaderContentCell.swift

UICollectionViewCell 子类,用于 Scroll 模式下承载页面内容视图。

class RDReaderContentCell: UICollectionViewCell {
    var containerView: UIView?  // 设置时自动添加到 contentView移除旧视图
}

10. PageCurl 子控制器RDReaderPageChildViewController

文件: RDReaderPageChildViewController.swift

UIViewController 子类,作为 UIPageViewController 的页面 VC。

class RDReaderPageChildViewController: UIViewController {
    var contentView: UIView?  // 内容视图,设置时自动安装到容器
    var pageNum: Int          // 对应页码

    init(contentView: UIView?, pageNum: Int = 0)
}

11. Extension 汇总

11.1 RDReaderView+PageCurl

实现 UIPageViewControllerDataSourceUIPageViewControllerDelegate

  • pageViewController(_:viewControllerBefore:) — 提供前一页 VCRTL 时逻辑反转)
  • pageViewController(_:viewControllerAfter:) — 提供后一页 VC
  • pageViewController(_:didFinishAnimating:...) — 完成动画后更新 currentPage
  • pageViewController(_:willTransitionTo:) — 即将翻页时预加载

特殊页码:

  • RDReaderView.blankPageNumInt.max)— 双页模式下的空白页
  • RDReaderView.blankEndPageNumInt.max - 1)— 末尾空白页

11.2 RDReaderView+CollectionView

实现 UICollectionViewDataSourceRDReaderFlowLayoutDelegateRDReaderFlowLayoutDataSoure

  • collectionView(_:cellForItemAt:) — 复用预加载视图或创建新 Cell
  • collectionView(_:numberOfItemsInSection:) — 返回总页数
  • pageNum(flowLayout:pageIndex:) — 滚动时更新当前页码

11.3 RDReaderView+ContentAccess

提供内容视图的注册、复用和查询:

/// 注册内容视图类型(类似 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

管理顶部/底部工具栏的安装、显示/隐藏动画:

/// 切换工具栏显示状态(点击中心区域触发)
func tapCenter()

/// 安装工具栏视图到指定位置
func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition)

/// 更新工具栏高度约束
func updateToolViewHeightConstraintsIfNeeded()

动画效果: 顶部工具栏从上方滑入,底部工具栏从下方滑入。


12. 设计模式总结

模式 应用
策略模式 DisplayType 切换 PageCurl / Scroll 两种翻页策略
适配器模式 RDReaderLegacyDataSourceAdapter 将旧 RDReaderDataSource 适配为 RDReaderPageProvider
命令队列 RDReaderPagingController 管理翻页请求队列
缓存签名 RDReaderPreloadController.CacheSignature 检测环境变化自动失效
关注点分离 Extension 将不同功能拆分到独立文件
纯函数 RDReaderSpreadResolverRDReaderTapRegionHandler 无状态计算

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() → 预加载周围页面