feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,27 +1,18 @@
|
||||
import UIKit
|
||||
|
||||
/// 翻页控制器:管理 UIPageViewController 的创建、切换、故障修复和翻页请求队列。
|
||||
///
|
||||
/// RDReaderView 持有此控制器,将 pageCurl 模式的容器管理职责下沉。
|
||||
struct RDReaderPagingController {
|
||||
/// 当前 pageCurl 翻页请求模型
|
||||
|
||||
struct PageTransitionRequest: Equatable {
|
||||
let pageNum: Int
|
||||
let animated: Bool
|
||||
}
|
||||
|
||||
/// 排队中的翻页请求(pageCurl 动画过程中收到新跳转时排队)
|
||||
var pendingTransitionRequest: PageTransitionRequest?
|
||||
|
||||
/// 是否正在执行页面转场动画
|
||||
var isTransitioning: Bool = false
|
||||
|
||||
/// UI 是否已经构建完成(避免重复构建)
|
||||
var didBuildUI = false
|
||||
|
||||
// MARK: - PageViewController 工厂
|
||||
|
||||
/// 创建带指定 spine 位置的 UIPageViewController
|
||||
static func createPageViewController(isDualPage: Bool) -> UIPageViewController {
|
||||
let options: [UIPageViewController.OptionsKey: Any]?
|
||||
if isDualPage {
|
||||
@@ -34,16 +25,12 @@ struct RDReaderPagingController {
|
||||
return pageVC
|
||||
}
|
||||
|
||||
// MARK: - 翻页请求队列管理
|
||||
|
||||
/// pageCurl 动画过程中如果收到新的跳转请求,则排队等待当前动画结束。
|
||||
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDReaderView.DisplayType) -> Bool {
|
||||
guard currentDisplayType == .pageCurl, isTransitioning else { return false }
|
||||
pendingTransitionRequest = request
|
||||
return true
|
||||
}
|
||||
|
||||
/// 收尾 pageCurl 转场,返回排队中的后续请求(如果有的话)。
|
||||
mutating func finishPageCurlTransition() -> PageTransitionRequest? {
|
||||
isTransitioning = false
|
||||
guard let pending = pendingTransitionRequest else { return nil }
|
||||
@@ -51,7 +38,6 @@ struct RDReaderPagingController {
|
||||
return pending
|
||||
}
|
||||
|
||||
/// 重置所有排队状态(用于故障修复)。
|
||||
mutating func resetPendingState() {
|
||||
isTransitioning = false
|
||||
pendingTransitionRequest = nil
|
||||
|
||||
@@ -1,49 +1,52 @@
|
||||
//
|
||||
// RDReaderPreloadController.swift
|
||||
// ReadViewSDK
|
||||
//
|
||||
// 文件职责:页面预加载控制器,负责提前渲染即将显示的页面视图以减少翻页卡顿。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 页面预加载控制器
|
||||
/// 负责管理页面视图的预加载和缓存,在当前页面基础上提前渲染前后若干页,
|
||||
/// 以减少翻页时的等待时间。支持仿真翻页和滚动模式下的不同复用策略。
|
||||
final class RDReaderPreloadController {
|
||||
/// 预加载半径,表示在当前页前后各预加载的页数,默认为 1
|
||||
|
||||
var radius: Int = 1
|
||||
|
||||
private let preloadHostView = UIView()
|
||||
|
||||
private var preloadedPageViews: [Int: UIView] = [:]
|
||||
|
||||
private var pageCurlCachedViews: [Int: UIView] = [:]
|
||||
|
||||
private var cacheSignature: CacheSignature?
|
||||
|
||||
/// 预加载环境参数,封装翻页模式、屏幕方向、页面尺寸等上下文信息
|
||||
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
|
||||
}
|
||||
|
||||
private struct CacheSignature: Equatable {
|
||||
|
||||
let displayType: RDReaderView.DisplayType
|
||||
|
||||
let isLandscape: Bool
|
||||
|
||||
let pagesPerScreen: Int
|
||||
|
||||
let boundsSize: CGSize
|
||||
}
|
||||
|
||||
/// 设置预加载宿主视图的 frame
|
||||
func setHostFrame(_ frame: CGRect) {
|
||||
preloadHostView.frame = frame
|
||||
}
|
||||
|
||||
/// 将预加载宿主视图添加到父视图中(如果尚未添加),置于最底层且不可见
|
||||
func ensureHostView(in parentView: UIView) {
|
||||
guard preloadHostView.superview == nil else { return }
|
||||
preloadHostView.isHidden = true
|
||||
@@ -54,12 +57,10 @@ final class RDReaderPreloadController {
|
||||
parentView.insertSubview(preloadHostView, at: 0)
|
||||
}
|
||||
|
||||
/// 根据当前环境初始化缓存签名,用于后续检测环境是否发生变化
|
||||
func initializeSignature(_ environment: Environment) {
|
||||
cacheSignature = currentCacheSignature(environment)
|
||||
}
|
||||
|
||||
/// 清除所有已缓存的页面视图并更新缓存签名
|
||||
func invalidate(environment: Environment) {
|
||||
pageCurlCachedViews.values.forEach { $0.removeFromSuperview() }
|
||||
preloadedPageViews.values.forEach { $0.removeFromSuperview() }
|
||||
@@ -68,7 +69,6 @@ final class RDReaderPreloadController {
|
||||
cacheSignature = currentCacheSignature(environment)
|
||||
}
|
||||
|
||||
/// 获取指定页码的页面视图用于显示,优先复用已缓存的视图
|
||||
func pageViewForDisplay(
|
||||
pageNum: Int,
|
||||
environment: Environment,
|
||||
@@ -80,14 +80,12 @@ final class RDReaderPreloadController {
|
||||
return view
|
||||
}
|
||||
|
||||
/// 取出指定页码的预加载视图并从缓存中移除,供外部复用
|
||||
func takePreloadedView(for pageNum: Int) -> UIView? {
|
||||
let preloaded = preloadedPageViews.removeValue(forKey: pageNum)
|
||||
preloaded?.removeFromSuperview()
|
||||
return preloaded
|
||||
}
|
||||
|
||||
/// 预加载当前页周围指定半径内的页面,清除不再需要的缓存视图
|
||||
func prime(
|
||||
around pageNum: Int,
|
||||
preferredForward: Bool? = nil,
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
//
|
||||
// RDReaderSpreadResolver.swift
|
||||
// ReadViewSDK
|
||||
//
|
||||
// 文件职责:页面展开(spread)解析器,负责计算双页模式下的页面配对和翻页目标。
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 页面展开解析器
|
||||
/// 处理横屏双页模式下的页码配对逻辑,包括封面页独占、双页对齐、
|
||||
/// 以及相邻页面跳转等计算。
|
||||
struct RDReaderSpreadResolver {
|
||||
/// 判断指定页码是否为全屏独占页面(如封面页在双页模式下独占一屏)
|
||||
|
||||
func isFullScreenPage(
|
||||
_ pageNum: Int,
|
||||
landscapeDualPageEnabled: Bool,
|
||||
@@ -22,8 +13,6 @@ struct RDReaderSpreadResolver {
|
||||
return pageNum == coverIndex
|
||||
}
|
||||
|
||||
/// 计算指定页码在双页模式下的左右页配对,返回 (左页, 右页?)。
|
||||
/// 封面页独占时单独处理,后续页面按两页一组配对。
|
||||
func dualPagePair(
|
||||
for pageNum: Int,
|
||||
totalPages: Int,
|
||||
@@ -45,7 +34,6 @@ struct RDReaderSpreadResolver {
|
||||
return (left, right)
|
||||
}
|
||||
|
||||
/// 计算从指定页码出发的下一个(或上一个)双页展开的起始页码
|
||||
func adjacentDualPage(
|
||||
from pageNum: Int,
|
||||
totalPages: Int,
|
||||
@@ -63,7 +51,6 @@ struct RDReaderSpreadResolver {
|
||||
return dualPagePair(for: prevEnd, totalPages: totalPages, coverPageIndex: coverPageIndex).left
|
||||
}
|
||||
|
||||
/// 计算下一页(或上一页)的页码,自动根据单页/双页模式选择合适的算法
|
||||
func nextPage(
|
||||
from currentPage: Int,
|
||||
totalPages: Int,
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
//
|
||||
// RDReaderTapRegionHandler.swift
|
||||
// ReadViewSDK
|
||||
//
|
||||
// 文件职责:点击区域判定处理器,将屏幕点击坐标映射为翻页或工具栏操作事件。
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
/// 点击区域判定处理器
|
||||
/// 将屏幕三等分为左、中、右三个区域,根据点击位置和工具栏可见状态
|
||||
/// 决定触发上一页、下一页还是切换工具栏。
|
||||
struct RDReaderTapRegionHandler {
|
||||
/// 根据点击坐标和视图尺寸,解析点击事件类型
|
||||
/// - 左1/3区域:上一页(工具栏可见时转为 center)
|
||||
/// - 中1/3区域:切换工具栏
|
||||
/// - 右1/3区域:下一页(工具栏可见时转为 center)
|
||||
|
||||
func resolveTapEvent(
|
||||
point: CGPoint,
|
||||
viewFrame: CGRect,
|
||||
|
||||
@@ -1,29 +1,10 @@
|
||||
//
|
||||
// RDReaderContentCell.swift
|
||||
// RDReaderDemo
|
||||
//
|
||||
// Created by yangsq on 2021/8/7.
|
||||
//
|
||||
// 文件职责:UICollectionViewCell 薄壳宿主,作为滚动模式下页面内容的容器。
|
||||
// 该 cell 本身不包含业务逻辑,仅持有 containerView 并通过自动布局将其填满。
|
||||
// 真正的内容视图由 RDReaderDataSource.pageContentView() 提供并赋值给 containerView。
|
||||
//
|
||||
// 架构位置:RDReaderView 四层架构中第三层的 cell 组件,被 UICollectionView 使用。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 内容 cell 薄壳宿主
|
||||
/// 作为 UICollectionView 滚动模式下的页面容器。
|
||||
/// 通过 ``containerView`` 属性持有实际内容视图,并在 layoutSubviews 中
|
||||
/// 将内容视图的 frame 设置为与 contentView 等大,实现自动填满。
|
||||
class RDReaderContentCell: UICollectionViewCell {
|
||||
/// 内部持有的内容视图引用
|
||||
|
||||
private var _containerView: UIView? = nil
|
||||
|
||||
/// 内容视图属性
|
||||
/// 设置时会自动将旧视图移除、新视图添加到 contentView 中
|
||||
/// 读取时返回当前持有的内容视图
|
||||
var containerView: UIView? {
|
||||
set {
|
||||
guard newValue !== _containerView else { return }
|
||||
@@ -39,8 +20,6 @@ class RDReaderContentCell: UICollectionViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// 布局子视图时调用
|
||||
/// 将 containerView 的 frame 设置为与 contentView 等大,实现自动填满
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
_containerView?.frame = CGRect(x: 0, y: 0, width: contentView.frame.width, height: contentView.frame.height)
|
||||
|
||||
@@ -1,56 +1,24 @@
|
||||
//
|
||||
// RDReaderFlowLayout.swift
|
||||
// RDReaderDemo
|
||||
//
|
||||
// Created by yangsq on 2021/8/6.
|
||||
//
|
||||
// 文件职责:自定义 UICollectionViewFlowLayout,支持阅读器的三种滚动布局模式。
|
||||
// 该文件实现了水平滚动(全屏宽 item + 分页)、垂直滚动(可变高度)和
|
||||
// 封面感知的双页布局(含封面页独占整屏逻辑)。
|
||||
//
|
||||
// 架构位置:RDReaderView 四层架构中第三层的布局组件,被 RDReaderView 持有和配置。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 流式布局数据源协议
|
||||
/// 提供垂直滚动模式下各页面的高度信息
|
||||
public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
|
||||
/// 返回垂直滚动模式下指定页的高度
|
||||
/// - Parameters:
|
||||
/// - flowLayout: 发起请求的布局对象
|
||||
/// - pageIndex: 页码索引
|
||||
/// - Returns: 该页的高度,nil 表示使用默认高度(collectionView.frame.height)
|
||||
|
||||
func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
|
||||
}
|
||||
|
||||
/// 流式布局代理协议
|
||||
/// 当当前可见页码变化时通知外部
|
||||
@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
|
||||
/// 页码变化回调
|
||||
/// - Parameters:
|
||||
/// - flowLayout: 发起回调的布局对象
|
||||
/// - pageIndex: 当前页码
|
||||
|
||||
func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
|
||||
}
|
||||
|
||||
/// 自定义流式布局,支持阅读器的三种滚动布局模式:
|
||||
/// - horizontalScroll:水平滚动,每屏1项(竖屏)或2项(横屏双页),支持分页
|
||||
/// - verticalScroll:垂直滚动,全宽项目,支持可变高度
|
||||
///
|
||||
/// 通过 ``displayType`` 属性切换布局模式。
|
||||
/// 通过 ``isLandscapeDualPage`` 和 ``coverPageIndex`` 控制横屏双页和封面页逻辑。
|
||||
public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
/// 布局模式,切换时自动使布局失效并重新计算
|
||||
var displayType: RDReaderView.DisplayType = .horizontalScroll {
|
||||
didSet {
|
||||
invalidateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否启用横屏双页模式
|
||||
/// 启用后水平滚动模式下每屏显示两项(各占半屏宽度)
|
||||
var isLandscapeDualPage: Bool = false {
|
||||
didSet {
|
||||
if oldValue != isLandscapeDualPage {
|
||||
@@ -59,8 +27,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 封面页索引
|
||||
/// 设置后封面页独占整屏,后续页面两两配对各占半屏
|
||||
var coverPageIndex: Int? = nil {
|
||||
didSet {
|
||||
if oldValue != coverPageIndex {
|
||||
@@ -69,11 +35,8 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一次 prepare 时的 bounds 尺寸,用于检测尺寸变化
|
||||
private var lastPreparedBoundsSize: CGSize = .zero
|
||||
|
||||
/// 每屏显示的页数
|
||||
/// 仅在水平滚动+横屏双页模式下返回2,其他情况返回1
|
||||
var pagesPerScreen: Int {
|
||||
guard isLandscapeDualPage else { return 1 }
|
||||
switch displayType {
|
||||
@@ -87,24 +50,14 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 流式布局数据源,提供垂直滚动模式的页面高度
|
||||
weak var dataSource: RDReaderFlowLayoutDataSoure? = nil
|
||||
/// 流式布局代理,接收页码变化通知
|
||||
|
||||
weak var delegate: RDReaderFlowLayoutDelegate? = nil
|
||||
|
||||
/// 是否在双页模式下有封面页独占
|
||||
private var hasCoverPageInDualMode: Bool {
|
||||
return pagesPerScreen > 1 && coverPageIndex != nil
|
||||
}
|
||||
|
||||
/// 封面感知的帧计算:根据索引计算 item 的 frame
|
||||
/// 封面页独占整屏,后续页面两两配对各占半屏
|
||||
/// - Parameters:
|
||||
/// - index: item 索引
|
||||
/// - screenWidth: 屏幕宽度
|
||||
/// - halfWidth: 半屏宽度
|
||||
/// - height: 屏幕高度
|
||||
/// - Returns: 该 item 的 frame
|
||||
private func coverAwareFrame(for index: Int, screenWidth: CGFloat, halfWidth: CGFloat, height: CGFloat) -> CGRect {
|
||||
guard let coverIndex = coverPageIndex else {
|
||||
let pairIdx = index / 2
|
||||
@@ -125,11 +78,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
return CGRect(x: x, y: 0, width: halfWidth, height: height)
|
||||
}
|
||||
|
||||
/// 封面感知的屏幕起始索引计算:根据滚动偏移量计算当前屏幕的第一个 item 索引
|
||||
/// - Parameters:
|
||||
/// - offset: 当前滚动偏移量
|
||||
/// - screenWidth: 屏幕宽度
|
||||
/// - Returns: 当前屏幕第一个 item 的索引
|
||||
private func coverAwareStartIndex(for offset: CGFloat, screenWidth: CGFloat) -> Int {
|
||||
let pps = pagesPerScreen
|
||||
guard let coverIdx = coverPageIndex, hasCoverPageInDualMode else {
|
||||
@@ -147,7 +95,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前可见的页码,值变化时通过 delegate 通知外部
|
||||
private var currentPage: Int = 0 {
|
||||
|
||||
didSet {
|
||||
@@ -157,15 +104,12 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化方法
|
||||
/// - Parameter displayType: 初始布局模式
|
||||
init(displayType: RDReaderView.DisplayType) {
|
||||
self.displayType = displayType
|
||||
super.init()
|
||||
|
||||
}
|
||||
|
||||
/// 布局准备阶段,配置 item 尺寸、滚动方向和分页行为
|
||||
public override func prepare() {
|
||||
super.prepare()
|
||||
guard let collectionView = self.collectionView else {
|
||||
@@ -203,8 +147,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
}
|
||||
|
||||
/// 判断 bounds 变化是否需要使布局失效
|
||||
/// 尺寸变化时总是返回 true,布局模式变化也返回 true
|
||||
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
if newBounds.size != lastPreparedBoundsSize {
|
||||
return true
|
||||
@@ -219,9 +161,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算 collectionView 的总内容尺寸
|
||||
/// 垂直滚动模式:累加所有页面高度
|
||||
/// 封面双页模式:封面占一屏 + 后续页面按两两配对计算屏幕数
|
||||
public override var collectionViewContentSize: CGSize {
|
||||
var size = super.collectionViewContentSize
|
||||
guard let collectionView = collectionView else {
|
||||
@@ -243,9 +182,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
return size
|
||||
}
|
||||
|
||||
/// 计算指定矩形区域内的布局属性
|
||||
/// 垂直滚动模式:根据可变高度定位每个 item
|
||||
/// 水平滚动模式:根据当前偏移量计算当前页码,清除 cell 阴影
|
||||
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
var attributes = super.layoutAttributesForElements(in: rect)
|
||||
guard let collectionView = collectionView else {
|
||||
@@ -279,7 +215,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if self.displayType == .horizontalScroll {
|
||||
let pps = pagesPerScreen
|
||||
|
||||
@@ -325,10 +260,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
return attributes
|
||||
}
|
||||
|
||||
/// 根据页码计算对应的 contentOffset
|
||||
/// 用于跳转到指定页面时设置滚动位置
|
||||
/// - Parameter count: 目标页码
|
||||
/// - Returns: 对应的 contentOffset 坐标
|
||||
func currentContentOffset(count: Int) -> CGPoint {
|
||||
guard let collectionView = collectionView else {
|
||||
return .zero
|
||||
@@ -368,7 +299,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@@ -1,33 +1,12 @@
|
||||
//
|
||||
// RDReaderGestureController.swift
|
||||
// RDReaderDemo
|
||||
//
|
||||
// Created by yangsq on 2021/8/10.
|
||||
//
|
||||
// 文件职责:手势控制器(当前为占位组件)。
|
||||
// 该控制器持有顶部和底部工具栏视图,预留了手势管理的扩展点。
|
||||
// 目前仅作为数据容器,未实现具体的手势识别逻辑。
|
||||
// 实际的手势识别由 RDReaderView 中的 tapGestureRecognizer 处理。
|
||||
//
|
||||
// 架构位置:RDReaderView 四层架构中第三层的辅助组件。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
/// 手势控制器(占位组件)
|
||||
/// 持有顶部和底部工具栏视图,预留手势管理扩展。
|
||||
/// 当前功能由 RDReaderView 直接处理,该类未被实际使用。
|
||||
class RDReaderGestureController: UIViewController {
|
||||
/// 顶部工具栏视图
|
||||
|
||||
var topToolView: UIView?
|
||||
/// 底部工具栏视图
|
||||
|
||||
var bottomToolView: UIView?
|
||||
|
||||
/// 初始化方法
|
||||
/// - Parameters:
|
||||
/// - topToolView: 顶部工具栏视图
|
||||
/// - bottomToolView: 底部工具栏视图
|
||||
init(topToolView: UIView?, bottomToolView: UIView?) {
|
||||
self.topToolView = topToolView
|
||||
self.bottomToolView = bottomToolView
|
||||
@@ -41,18 +20,6 @@ class RDReaderGestureController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
// MARK: - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
// Get the new view controller using segue.destination.
|
||||
// Pass the selected object to the new view controller.
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
//
|
||||
// RDReaderPageChildViewController.swift
|
||||
// RDReaderDemo
|
||||
//
|
||||
// Created by yangsq on 2021/8/9.
|
||||
//
|
||||
// 文件职责:UIPageViewController 的子控制器,持有单页的内容视图和页码。
|
||||
// 在 pageCurl 仿真翻页模式下,每个页面由一个 RDReaderPageChildViewController 管理,
|
||||
// 通过 contentContainerView 承载实际内容视图,并保持页码引用用于翻页状态追踪。
|
||||
//
|
||||
// 架构位置:RDReaderView 四层架构中第三层的子控制器组件,被 UIPageViewController 使用。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 仿真翻页模式下的页面子控制器
|
||||
/// 作为 UIPageViewController 的子控制器,每个实例管理一页内容。
|
||||
/// 持有 ``contentView``(实际内容视图)和 ``pageNum``(页码标识),
|
||||
/// 通过 contentContainerView 将内容视图填满控制器视图。
|
||||
class RDReaderPageChildViewController: UIViewController {
|
||||
/// 内容容器视图,作为 contentView 的父视图
|
||||
|
||||
private let contentContainerView = UIView()
|
||||
|
||||
/// 实际内容视图
|
||||
/// 设置时如果视图已加载,会自动重新安装到 contentContainerView 中
|
||||
var contentView: UIView? {
|
||||
didSet {
|
||||
guard isViewLoaded else { return }
|
||||
@@ -30,21 +12,14 @@ class RDReaderPageChildViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 该控制器管理的页码标识
|
||||
var pageNum: Int = 0
|
||||
|
||||
/// 初始化方法
|
||||
/// - Parameters:
|
||||
/// - contentView: 要显示的内容视图
|
||||
/// - pageNum: 该页的页码(默认0)
|
||||
init(contentView: UIView?, pageNum: Int = 0) {
|
||||
self.contentView = contentView
|
||||
self.pageNum = pageNum
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
/// 加载视图时调用
|
||||
/// 创建透明背景的根视图,添加 contentContainerView,并安装内容视图
|
||||
override func loadView() {
|
||||
view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
@@ -61,11 +36,8 @@ class RDReaderPageChildViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
/// 安装内容视图到 contentContainerView 中
|
||||
/// 先移除 contentContainerView 中的所有子视图,再将 contentView 添加并填满
|
||||
private func installContentView() {
|
||||
contentContainerView.subviews.forEach { $0.removeFromSuperview() }
|
||||
guard let contentView else { return }
|
||||
@@ -75,15 +47,4 @@ class RDReaderPageChildViewController: UIViewController {
|
||||
contentContainerView.addSubview(contentView)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
// MARK: - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
// Get the new view controller using segue.destination.
|
||||
// Pass the selected object to the new view controller.
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
//
|
||||
// RDReaderView+CollectionView.swift
|
||||
// ReadViewSDK
|
||||
//
|
||||
// 文件职责:UICollectionView 数据源和自定义布局代理实现,处理水平滚动和垂直滚动两种翻页模式。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// UICollectionView 数据源和自定义布局代理实现
|
||||
/// 处理水平滚动和垂直滚动两种翻页模式
|
||||
extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate, RDReaderFlowLayoutDataSoure {
|
||||
/// 配置 UICollectionViewCell
|
||||
/// 通过 DataSource 获取页面内容视图,支持 cell 复用和预加载视图复用
|
||||
/// RTL 水平模式下翻转 cell 内容使文字方向正常
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
|
||||
if let identifer = pageReuseIdentifier(for: indexPath.row) {
|
||||
@@ -20,7 +10,7 @@ extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate,
|
||||
let preloadedView = preloadController.takePreloadedView(for: indexPath.row)
|
||||
let reusableView = cell.containerView ?? preloadedView
|
||||
cell.containerView = contentViewForPage(indexPath.row, reusableView: reusableView)
|
||||
// RTL 水平模式:翻转 cell 内容使文字方向正常(collectionView 已整体翻转)
|
||||
|
||||
if pageDirection == .rightToLeft && currentDisplayType != .verticalScroll {
|
||||
cell.contentView.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
} else {
|
||||
@@ -34,12 +24,10 @@ extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate,
|
||||
return cell
|
||||
}
|
||||
|
||||
/// 返回每组的 item 数量,即总页数
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return numberOfPages()
|
||||
}
|
||||
|
||||
/// 布局代理回调:当前可见页码变化时通知
|
||||
public func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int) {
|
||||
if currentPage >= 0, currentPage != pageIndex {
|
||||
predictedPageDirection = pageIndex >= currentPage
|
||||
@@ -48,8 +36,6 @@ extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate,
|
||||
primePageCache(around: pageIndex, preferredForward: predictedPageDirection)
|
||||
}
|
||||
|
||||
/// 布局数据源回调:返回垂直滚动模式下指定页的高度
|
||||
/// 当前返回 nil 使用默认高度
|
||||
public func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
//
|
||||
// RDReaderView+ContentAccess.swift
|
||||
// ReadViewSDK
|
||||
//
|
||||
// 文件职责:内容视图注册、复用和页面内容访问,提供类似 UITableView 的 register/dequeueReusable 机制。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 内容视图注册和复用扩展
|
||||
/// 提供类似 UITableView 的 register/dequeueReusable 机制
|
||||
extension RDReaderView {
|
||||
/// 注册内容视图类型
|
||||
|
||||
public func register(contentView: UIView.Type, contentViewWithReuseIdentifier identifier: String) {
|
||||
contentViews[identifier] = contentView
|
||||
collectionView.register(RDReaderContentCell.self, forCellWithReuseIdentifier: identifier)
|
||||
}
|
||||
|
||||
/// 获取可复用的内容视图
|
||||
/// 优先从当前显示的 cell 中获取,其次创建新实例
|
||||
public func dequeueReusableContentView(withReuseIdentifier identifier: String, for pageNum: Int) -> UIView {
|
||||
if self.currentDisplayType != .pageCurl, let cell = self.collectionView.cellForItem(at: IndexPath(row: pageNum, section: 0)) as? RDReaderContentCell, let containerView = cell.containerView {
|
||||
return containerView
|
||||
@@ -28,8 +18,6 @@ extension RDReaderView {
|
||||
return contentView
|
||||
}
|
||||
|
||||
/// 获取指定页码的内容视图
|
||||
/// pageCurl 模式从 PageViewController 获取,滚动模式从 CollectionView cell 获取
|
||||
public func pageContentView(pageNum: Int) -> UIView? {
|
||||
if currentDisplayType == .pageCurl {
|
||||
return (self.pageViewController.viewControllers?.first as? RDReaderPageChildViewController)?.contentView
|
||||
@@ -39,8 +27,6 @@ extension RDReaderView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回当前阅读模式下单页内容容器的实际尺寸。
|
||||
/// 优先使用已经显示出来的内容视图尺寸;若页面尚未装载,则回退到当前模式下的理论单页尺寸。
|
||||
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize {
|
||||
let targetPage = pageNum ?? (currentPage >= 0 ? currentPage : nil)
|
||||
if let targetPage,
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
//
|
||||
// RDReaderView+PageCurl.swift
|
||||
// ReadViewSDK
|
||||
//
|
||||
// 文件职责:UIPageViewController 数据源和代理实现,处理仿真翻页模式下的页面数据供给和翻页事件。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// UIPageViewController 数据源和代理实现
|
||||
/// 处理仿真翻页模式下的页面数据供给和翻页事件
|
||||
extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
|
||||
|
||||
/// 创建指定页码的子控制器
|
||||
/// 空白页返回空视图,正常页从缓存或数据源获取内容
|
||||
private func makeSinglePageChildVC(for pageNum: Int) -> RDReaderPageChildViewController {
|
||||
if pageNum == RDReaderView.blankPageNum || pageNum == RDReaderView.blankEndPageNum {
|
||||
return RDReaderPageChildViewController(contentView: UIView(), pageNum: pageNum)
|
||||
@@ -21,12 +11,9 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
|
||||
return RDReaderPageChildViewController(contentView: contentView, pageNum: pageNum)
|
||||
}
|
||||
|
||||
/// 计算某页的"下一页"页码
|
||||
/// 考虑封面页独占和末尾空白页配对的情况
|
||||
private func nextPageNum(after pageNum: Int, isDualPage: Bool) -> Int? {
|
||||
let totalPages = numberOfPages()
|
||||
|
||||
// 末尾空白页之后没有更多页
|
||||
if pageNum == RDReaderView.blankEndPageNum {
|
||||
return nil
|
||||
}
|
||||
@@ -46,7 +33,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
|
||||
return next
|
||||
}
|
||||
|
||||
// pageNum 是最后一页,检查在双页模式下是否需要空白页配对
|
||||
if isDualPage {
|
||||
if let coverIndex = coverPageIndex {
|
||||
let adjustedIndex = pageNum - (coverIndex + 1)
|
||||
@@ -62,8 +48,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 计算某页的"上一页"页码
|
||||
/// 考虑封面页独占和末尾空白页配对的情况
|
||||
private func prevPageNum(before pageNum: Int, isDualPage: Bool) -> Int? {
|
||||
if pageNum == RDReaderView.blankEndPageNum {
|
||||
let totalPages = numberOfPages()
|
||||
@@ -81,8 +65,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
|
||||
return prev >= 0 ? prev : nil
|
||||
}
|
||||
|
||||
/// UIPageViewController 数据源:返回当前页之前(左/上)的页面控制器
|
||||
/// RTL 模式下 before/after 语义互换:before(向右翻)= 下一页
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard let vc = viewController as? RDReaderPageChildViewController else { return nil }
|
||||
let isDualPage = landscapeDualPageEnabled && isLandscape
|
||||
@@ -98,8 +80,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
|
||||
return targetVC
|
||||
}
|
||||
|
||||
/// UIPageViewController 数据源:返回当前页之后(右/下)的页面控制器
|
||||
/// RTL 模式下 before/after 语义互换
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard let vc = viewController as? RDReaderPageChildViewController else { return nil }
|
||||
let isDualPage = landscapeDualPageEnabled && isLandscape
|
||||
@@ -115,8 +95,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
|
||||
return targetVC
|
||||
}
|
||||
|
||||
/// UIPageViewController 代理:翻页动画完成回调
|
||||
/// 更新当前页码,触发预加载,并检测 PageViewController 异常状态
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||
if completed, let firstVC = pageViewController.viewControllers?.first as? RDReaderPageChildViewController {
|
||||
let pn = firstVC.pageNum
|
||||
@@ -130,8 +108,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
|
||||
finishPageCurlTransition()
|
||||
}
|
||||
|
||||
/// UIPageViewController 代理:即将开始转场动画
|
||||
/// 记录预测的翻页方向,用于优化预加载策略
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||
pagingController.isTransitioning = true
|
||||
willTransitionToViewController = pendingViewControllers.first
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
//
|
||||
// RDReaderView+ToolView.swift
|
||||
// ReadViewSDK
|
||||
//
|
||||
// 文件职责:工具栏管理扩展,处理工具栏的显示/隐藏动画、安装和布局约束。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 工具栏管理扩展:处理工具栏的显示/隐藏动画、安装、布局
|
||||
extension RDReaderView {
|
||||
|
||||
/// 点击屏幕中央区域,切换工具栏的显示/隐藏,带动画效果
|
||||
func tapCenter() {
|
||||
refreshToolViewsFromProviderIfNeeded()
|
||||
isShowToolView = !isShowToolView
|
||||
@@ -58,7 +50,6 @@ extension RDReaderView {
|
||||
onToolViewVisibilityChanged?(isShowToolView)
|
||||
}
|
||||
|
||||
/// 判断点击命中的视图是否在指定工具栏内,用于决定是否拦截点击事件
|
||||
func isHitView(_ hitView: UIView?, inside toolView: UIView, point: CGPoint) -> Bool {
|
||||
if toolView.frame.contains(point) {
|
||||
return true
|
||||
@@ -68,15 +59,13 @@ extension RDReaderView {
|
||||
return hitView === toolView || hitView.isDescendant(of: toolView)
|
||||
}
|
||||
|
||||
/// 工具栏位置枚举
|
||||
enum ToolViewPosition {
|
||||
/// 顶部工具栏(如标题栏、导航栏)
|
||||
|
||||
case top
|
||||
/// 底部工具栏(如进度条、操作按钮)
|
||||
|
||||
case bottom
|
||||
}
|
||||
|
||||
/// 将工具栏安装到指定位置并设置布局约束,已安装则跳过
|
||||
func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition) {
|
||||
guard toolView.superview !== self else { return }
|
||||
|
||||
@@ -109,7 +98,6 @@ extension RDReaderView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新顶部和底部工具栏的高度约束,通常在 safeAreaInsets 变化时调用
|
||||
func updateToolViewHeightConstraintsIfNeeded() {
|
||||
topToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .top)
|
||||
bottomToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .bottom)
|
||||
|
||||
@@ -1,58 +1,19 @@
|
||||
//
|
||||
// RDReaderView.swift
|
||||
// RDReaderDemo
|
||||
//
|
||||
// Created by yangsq on 2021/8/6.
|
||||
//
|
||||
// 文件职责:核心翻页容器视图,RDReaderView 模块的入口和中枢。
|
||||
// 该文件定义了阅读器的核心 UIView 子类 RDReaderView,负责:
|
||||
// 1. 管理三种翻页模式(仿真翻页、水平滚动、垂直滚动)
|
||||
// 2. 实现手势分区逻辑(左1/3上一页、中1/3工具栏、右1/3下一页)
|
||||
// 3. 支持横屏双页显示(含封面页独占和页码配对算法)
|
||||
// 4. 支持从左往右(LTR)和从右往左(RTL)两种翻页方向
|
||||
// 5. 提供 DataSource/Delegate 协议供上层控制器实现数据供给和事件回调
|
||||
// 6. 管理页面预加载和缓存机制
|
||||
//
|
||||
// 架构位置:RDReaderView 四层架构中的第三层(翻页容器层)
|
||||
// EPUBCore(解析引擎)→ EPUBTextRendering(文本渲染)→ RDReaderView(翻页容器)→ EPUBUI(读者 UI)
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
/// 核心翻页容器视图
|
||||
/// RDReaderView 模块的核心类,负责管理三种翻页模式的切换、手势识别、
|
||||
/// 横屏双页显示、页面缓存预加载等核心功能。
|
||||
///
|
||||
/// 使用方式:
|
||||
/// 1. 设置 ``dataSource`` 提供页面数据
|
||||
/// 2. 设置 ``delegate`` 接收翻页事件
|
||||
/// 3. 调用 ``switchReaderDisplayType(_:)`` 切换翻页模式
|
||||
/// 4. 调用 ``reloadData()`` 刷新数据
|
||||
///
|
||||
/// 支持的功能:
|
||||
/// - 三种翻页模式:pageCurl / horizontalScroll / verticalScroll
|
||||
/// - 横屏双页显示(含封面页独占逻辑)
|
||||
/// - RTL 翻页方向支持
|
||||
/// - 手势分区(左翻/工具栏/右翻)
|
||||
/// - 页面预加载和缓存
|
||||
public class RDReaderView: UIView {
|
||||
|
||||
/// 屏幕点击区域枚举,用于手势分区逻辑
|
||||
/// 将屏幕水平三等分,分别响应不同的手势事件
|
||||
enum TapEvent {
|
||||
/// 无操作
|
||||
|
||||
case none
|
||||
/// 左1/3区域:上一页(RTL 时为下一页)
|
||||
|
||||
case left
|
||||
/// 中1/3区域:切换工具栏显示/隐藏
|
||||
|
||||
case center
|
||||
/// 右1/3区域:下一页(RTL 时为上一页)
|
||||
|
||||
case right
|
||||
}
|
||||
|
||||
/// 仿真翻页模式使用的 UIPageViewController
|
||||
/// 通过 pageCurl 转场样式实现仿真翻书效果
|
||||
lazy var pageViewController: UIPageViewController = {
|
||||
let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil)
|
||||
pageVC.delegate = self
|
||||
@@ -60,7 +21,6 @@ public class RDReaderView: UIView {
|
||||
return pageVC
|
||||
}()
|
||||
|
||||
/// 自定义流式布局,支持水平滚动、垂直滚动和水平覆盖滚动三种布局模式
|
||||
lazy var layout: RDReaderFlowLayout = {
|
||||
let layout = RDReaderFlowLayout(displayType: .horizontalScroll)
|
||||
layout.dataSource = self
|
||||
@@ -68,8 +28,6 @@ public class RDReaderView: UIView {
|
||||
return layout
|
||||
}()
|
||||
|
||||
/// 滚动模式使用的 UICollectionView
|
||||
/// 用于 horizontalScroll 和 verticalScroll 两种翻页模式
|
||||
lazy var collectionView: UICollectionView = {
|
||||
|
||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
@@ -77,14 +35,15 @@ public class RDReaderView: UIView {
|
||||
collectionView.accessibilityIdentifier = "epub.reader.paging"
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
private let spreadResolver = RDReaderSpreadResolver()
|
||||
|
||||
private let tapRegionHandler = RDReaderTapRegionHandler()
|
||||
|
||||
let preloadController = RDReaderPreloadController()
|
||||
|
||||
var pagingController = RDReaderPagingController()
|
||||
|
||||
/// 当前显示的页码
|
||||
/// 值变化时会通过 delegate 回调通知上层控制器
|
||||
/// 初始值为 -1 表示尚未加载任何页面
|
||||
public var currentPage: Int = -1 {
|
||||
didSet {
|
||||
if let delegate = delegate, currentPage != oldValue, delegate.responds(to: #selector(RDReaderDelegate.pageNum(readerView:pageNum:))) {
|
||||
@@ -93,16 +52,12 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 全屏点击手势识别器
|
||||
/// 用于检测用户点击屏幕的区域,触发翻页或工具栏切换
|
||||
private(set) lazy var tapGestureRecognizer: UITapGestureRecognizer = {
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(tap:)))
|
||||
tap.delegate = self
|
||||
return tap
|
||||
}()
|
||||
|
||||
/// 当前点击事件类型
|
||||
/// 值变化时会根据事件类型执行翻页或工具栏切换逻辑
|
||||
private var tapEvent: TapEvent = .none {
|
||||
didSet {
|
||||
let isRTL = pageDirection == .rightToLeft
|
||||
@@ -123,8 +78,6 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 翻到下一页
|
||||
/// 在双页模式下使用 adjacentDualPage 计算目标页码,单页模式直接 +1
|
||||
private func goNextPage() {
|
||||
let totalPages = numberOfPages()
|
||||
if let target = spreadResolver.nextPage(
|
||||
@@ -138,8 +91,6 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 翻到上一页
|
||||
/// 在双页模式下使用 adjacentDualPage 计算目标页码,单页模式直接 -1
|
||||
private func goPreviousPage() {
|
||||
let totalPages = numberOfPages()
|
||||
if let target = spreadResolver.nextPage(
|
||||
@@ -154,30 +105,29 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
|
||||
private let legacyDataSourceAdapter = RDReaderLegacyDataSourceAdapter()
|
||||
/// 数据源代理,提供页面数量、内容视图和工具栏
|
||||
|
||||
public weak var dataSource: RDReaderDataSource? {
|
||||
didSet {
|
||||
legacyDataSourceAdapter.dataSource = dataSource
|
||||
}
|
||||
}
|
||||
/// 新的通用分页内容提供者,优先级高于 ``dataSource``。
|
||||
|
||||
public weak var pageProvider: RDReaderPageProvider?
|
||||
/// 代理,接收翻页和横竖屏切换事件
|
||||
|
||||
public weak var delegate: RDReaderDelegate? = nil
|
||||
/// 当前翻页模式,默认为仿真翻页
|
||||
|
||||
public var currentDisplayType: RDReaderView.DisplayType = .pageCurl
|
||||
/// 工具栏显示/隐藏动画时长,默认 0.3 秒
|
||||
|
||||
public var toolViewAnimationDuration: TimeInterval = 0.3
|
||||
/// 工具栏可见性变化回调,参数为是否可见
|
||||
|
||||
var onToolViewVisibilityChanged: ((Bool) -> Void)?
|
||||
/// 搜索栏视图,点击时不触发 tapCenter
|
||||
|
||||
var searchBarView: UIView?
|
||||
/// 是否启用横屏双页显示
|
||||
|
||||
public var landscapeDualPageEnabled: Bool = false
|
||||
/// 翻页方向,默认从左往右(适用于中文/英文书籍)
|
||||
|
||||
public var pageDirection: RDReaderView.PageDirection = .leftToRight
|
||||
/// 横屏双页模式下,封面页的索引。设置后该页在横屏时独占一屏,后续页面两两配对。
|
||||
/// 设为 nil 表示没有封面页(所有页面正常两两配对:0+1, 2+3, 4+5...)。
|
||||
|
||||
public var coverPageIndex: Int? = nil
|
||||
|
||||
private var resolvedPageProvider: RDReaderPageProvider? {
|
||||
@@ -213,27 +163,20 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否启用了封面页独占
|
||||
private var hasCoverPage: Bool {
|
||||
return coverPageIndex != nil
|
||||
}
|
||||
|
||||
/// 当前是否横屏(宽度 > 高度)
|
||||
var isLandscape: Bool {
|
||||
return bounds.width > bounds.height
|
||||
}
|
||||
|
||||
/// 每屏显示的页数
|
||||
/// 未启用双页、垂直滚动模式或竖屏时返回1,横屏双页模式返回2
|
||||
public var pagesPerScreen: Int {
|
||||
if !landscapeDualPageEnabled { return 1 }
|
||||
if currentDisplayType == .verticalScroll { return 1 }
|
||||
return isLandscape ? 2 : 1
|
||||
}
|
||||
|
||||
/// 判断某页在横屏双页模式下是否独占一屏(封面页)
|
||||
/// - Parameter pageNum: 页码
|
||||
/// - Returns: 是否独占一屏
|
||||
public func isFullScreenPage(_ pageNum: Int) -> Bool {
|
||||
spreadResolver.isFullScreenPage(
|
||||
pageNum,
|
||||
@@ -243,10 +186,6 @@ public class RDReaderView: UIView {
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据逻辑页码计算横屏双页模式下的配对信息
|
||||
/// 封面页独占一屏,后续页面两两配对
|
||||
/// - Parameter pageNum: 逻辑页码
|
||||
/// - Returns: (左页码, 右页码(可选,nil表示右页为空白))
|
||||
private func dualPagePair(for pageNum: Int) -> (left: Int, right: Int?) {
|
||||
let totalPages = numberOfPages()
|
||||
return spreadResolver.dualPagePair(
|
||||
@@ -256,11 +195,6 @@ public class RDReaderView: UIView {
|
||||
)
|
||||
}
|
||||
|
||||
/// 计算横屏双页下,某页往前/后翻一屏后的起始页码
|
||||
/// - Parameters:
|
||||
/// - pageNum: 当前页码
|
||||
/// - forward: 是否向后翻(true=下一页,false=上一页)
|
||||
/// - Returns: 目标页码,nil 表示到头了
|
||||
private func adjacentDualPage(from pageNum: Int, forward: Bool) -> Int? {
|
||||
let totalPages = numberOfPages()
|
||||
return spreadResolver.adjacentDualPage(
|
||||
@@ -271,60 +205,63 @@ public class RDReaderView: UIView {
|
||||
)
|
||||
}
|
||||
|
||||
/// 将页码钳制到当前书籍的合法范围内
|
||||
private func clampedPageNumber(_ pageNum: Int) -> Int? {
|
||||
let totalPages = numberOfPages()
|
||||
guard totalPages > 0 else { return nil }
|
||||
return min(max(pageNum, 0), totalPages - 1)
|
||||
}
|
||||
|
||||
/// 上一次检测到的横竖屏状态,用于在 layoutSubviews 中检测方向变化
|
||||
private var previousIsLandscape: Bool?
|
||||
|
||||
/// 已注册的内容视图类型字典,key 为重用标识符
|
||||
var contentViews = [String : UIView.Type]()
|
||||
/// pageCurl 模式下,即将向前翻转到的子控制器
|
||||
|
||||
var willPreviousTransitionToViewController: UIViewController? = nil
|
||||
/// pageCurl 模式下,即将向后翻转到的子控制器
|
||||
|
||||
var willNextTransitionToViewController: UIViewController? = nil
|
||||
/// pageCurl 模式下,正在转场的目标控制器
|
||||
|
||||
var willTransitionToViewController: UIViewController? = nil
|
||||
/// 顶部工具栏视图
|
||||
|
||||
var topToolView: UIView?
|
||||
/// 底部工具栏视图
|
||||
|
||||
var bottomToolView: UIView?
|
||||
/// 顶部工具栏高度约束
|
||||
|
||||
var topToolViewHeightConstraint: NSLayoutConstraint?
|
||||
/// 底部工具栏高度约束
|
||||
|
||||
var bottomToolViewHeightConstraint: NSLayoutConstraint?
|
||||
/// 工具栏是否正在显示
|
||||
|
||||
var isShowToolView: Bool = false
|
||||
/// 是否正在执行页面转场动画
|
||||
|
||||
private var isTransitioning: Bool {
|
||||
get { pagingController.isTransitioning }
|
||||
set { pagingController.isTransitioning = newValue }
|
||||
}
|
||||
/// UI 是否已经构建完成(避免重复构建)
|
||||
|
||||
private var didBuildUI: Bool {
|
||||
get { pagingController.didBuildUI }
|
||||
set { pagingController.didBuildUI = newValue }
|
||||
}
|
||||
/// 预测的翻页方向(true=向前,false=向后),用于优化预加载方向
|
||||
|
||||
var predictedPageDirection: Bool?
|
||||
/// 预加载半径:当前页前后各预加载几屏,默认 1 屏
|
||||
|
||||
public var preloadRadius: Int {
|
||||
get { preloadController.radius }
|
||||
set { preloadController.radius = newValue }
|
||||
}
|
||||
|
||||
/// 用于 pageCurl 双页模式下封面页旁边的空白页标识
|
||||
static let blankPageNum = Int.max
|
||||
/// 用于 pageCurl 双页模式下末尾不成对页旁边的空白页标识
|
||||
|
||||
static let blankEndPageNum = Int.max - 1
|
||||
|
||||
/// 当 pageCurl 正在转场时,后续跳转请求会先排队,待当前动画稳定后再执行。
|
||||
private typealias PageTransitionRequest = RDReaderPagingController.PageTransitionRequest
|
||||
|
||||
private let registeredSelectionLongPressRecognizers = NSHashTable<UILongPressGestureRecognizer>.weakObjects()
|
||||
|
||||
private let selectionTapSuppressedContentViews = NSHashTable<UIView>.weakObjects()
|
||||
|
||||
private let selectionPagingSuppressedContentViews = NSHashTable<UIView>.weakObjects()
|
||||
|
||||
private var isPagingInteractionSuppressed = false
|
||||
|
||||
private var pendingTransitionRequest: PageTransitionRequest? {
|
||||
get { pagingController.pendingTransitionRequest }
|
||||
set { pagingController.pendingTransitionRequest = newValue }
|
||||
@@ -336,8 +273,6 @@ public class RDReaderView: UIView {
|
||||
isAccessibilityElement = false
|
||||
}
|
||||
|
||||
/// 布局子视图时调用
|
||||
/// 检测横竖屏方向变化,触发相应的布局更新和页面重建
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard bounds.width > 0, bounds.height > 0 else { return }
|
||||
@@ -346,8 +281,7 @@ public class RDReaderView: UIView {
|
||||
let nowLandscape = isLandscape
|
||||
if let prev = previousIsLandscape, prev != nowLandscape {
|
||||
previousIsLandscape = nowLandscape
|
||||
// 延迟到下一个 RunLoop 执行,避免在 layoutSubviews 中嵌套触发 reloadData/layoutIfNeeded
|
||||
// 造成 collectionView 中间态尺寸不一致的问题
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.orientationChanged(isNowLandscape: nowLandscape)
|
||||
@@ -358,33 +292,28 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 横竖屏切换处理
|
||||
/// 根据当前翻页模式执行不同的重建策略:
|
||||
/// - pageCurl:重建 UIPageViewController(因为 spineLocation 只能在初始化时设置)
|
||||
/// - 滚动模式:禁用动画,重新加载数据并恢复滚动位置
|
||||
/// - Parameter isNowLandscape: 当前是否为横屏
|
||||
private func orientationChanged(isNowLandscape: Bool) {
|
||||
let savedPage = max(0, currentPage)
|
||||
// 1. 通知代理方向变化。正文级重分页由上层控制器统一接管,这里只保留容器级刷新钩子。
|
||||
|
||||
delegate?.readerViewOrientationWillChange?(readerView: self, isLandscape: isNowLandscape)
|
||||
// 2. 更新布局的横屏双页标记
|
||||
|
||||
layout.isLandscapeDualPage = landscapeDualPageEnabled && isNowLandscape
|
||||
layout.coverPageIndex = coverPageIndex
|
||||
|
||||
switch currentDisplayType {
|
||||
case .pageCurl:
|
||||
// 仿真翻页:通过 spineLocation 原生支持双页,需要重建 PageViewController
|
||||
|
||||
rebuildPageViewController()
|
||||
transitionToPage(pageNum: savedPage)
|
||||
primePageCache(around: savedPage, preferredForward: predictedPageDirection)
|
||||
default:
|
||||
// 滚动模式:禁用动画防止旋转过渡中出现尺寸抖动
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
// 强制使布局完全失效并重新计算
|
||||
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
collectionView.reloadData()
|
||||
collectionView.layoutIfNeeded()
|
||||
// 3. 根据保存的页码恢复滚动位置
|
||||
|
||||
let totalPages = numberOfPages()
|
||||
let safePage = min(savedPage, max(0, totalPages - 1))
|
||||
let targetOffset = layout.currentContentOffset(count: safePage)
|
||||
@@ -394,9 +323,6 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建带指定 spine 位置的 UIPageViewController
|
||||
/// - Parameter isDualPage: 是否双页模式(横屏时书脊在中间:.mid)
|
||||
/// - Returns: 配置好的 UIPageViewController 实例
|
||||
private func createPageViewController(isDualPage: Bool) -> UIPageViewController {
|
||||
let options: [UIPageViewController.OptionsKey: Any]?
|
||||
if isDualPage {
|
||||
@@ -411,9 +337,6 @@ public class RDReaderView: UIView {
|
||||
return pageVC
|
||||
}
|
||||
|
||||
/// 重建 UIPageViewController
|
||||
/// 横竖屏切换时需要重建,因为 spineLocation 只能在初始化时设置
|
||||
/// 先移除旧的 PageViewController,再创建新的并添加到父控制器
|
||||
private func rebuildPageViewController() {
|
||||
detachPageViewControllerIfNeeded()
|
||||
|
||||
@@ -423,12 +346,6 @@ public class RDReaderView: UIView {
|
||||
attachPageViewControllerIfNeeded()
|
||||
}
|
||||
|
||||
// MARK: - UIPageViewController Fault Detection
|
||||
|
||||
/// 检测 UIPageViewController 是否处于异常状态
|
||||
/// 异常状态包括:子控制器数量不对、页码不匹配等
|
||||
/// - Parameter pageVC: 要检测的 UIPageViewController
|
||||
/// - Returns: true 表示存在异常,需要修复
|
||||
private func detectPageViewControllerFault(_ pageVC: UIPageViewController) -> Bool {
|
||||
guard currentDisplayType == .pageCurl else { return false }
|
||||
let expectedCount = (landscapeDualPageEnabled && isLandscape) ? 2 : 1
|
||||
@@ -453,8 +370,6 @@ public class RDReaderView: UIView {
|
||||
|| childViewControllers[1].pageNum != expectedRightPage
|
||||
}
|
||||
|
||||
/// 修复 UIPageViewController 异常状态
|
||||
/// 清空缓存、重建 PageViewController 并重新定位到当前页
|
||||
private func patchPageViewControllerFault() {
|
||||
guard currentPage >= 0 else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
@@ -469,12 +384,10 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// pageCurl 动画过程中如果收到新的跳转请求,则排队等待当前动画结束。
|
||||
private func shouldQueuePageTransition(_ request: PageTransitionRequest) -> Bool {
|
||||
pagingController.shouldQueuePageTransition(request, currentDisplayType: currentDisplayType)
|
||||
}
|
||||
|
||||
/// 收尾 pageCurl 转场,并尝试执行排队中的后续跳转。
|
||||
func finishPageCurlTransition(repairFaultIfNeeded: Bool = true) {
|
||||
willPreviousTransitionToViewController = nil
|
||||
willNextTransitionToViewController = nil
|
||||
@@ -490,8 +403,6 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preload / Forecast
|
||||
|
||||
private var preloadEnvironment: RDReaderPreloadController.Environment {
|
||||
RDReaderPreloadController.Environment(
|
||||
displayType: currentDisplayType,
|
||||
@@ -531,7 +442,6 @@ public class RDReaderView: UIView {
|
||||
contentViewForPage(pageNum, reusableView: reusableView)
|
||||
}
|
||||
|
||||
/// 视图被添加到父视图时调用,触发 UI 构建
|
||||
public override func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
|
||||
@@ -540,8 +450,6 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 pageViewController 添加到父控制器和视图层级
|
||||
/// 如果已在父控制器中则跳过,确保生命周期方法正确调用
|
||||
private func attachPageViewControllerIfNeeded() {
|
||||
guard let parentViewController = self.ss_superViewController else { return }
|
||||
|
||||
@@ -571,8 +479,6 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从父控制器和视图层级中移除 pageViewController
|
||||
/// 确保 willMove/didMove 生命周期方法正确调用
|
||||
private func detachPageViewControllerIfNeeded() {
|
||||
if pageViewController.parent != nil {
|
||||
pageViewController.willMove(toParent: nil)
|
||||
@@ -583,8 +489,6 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建 UI 界面
|
||||
/// 首次调用时注册 cell、添加手势识别器、根据当前翻页模式添加对应视图
|
||||
private func makeUI() {
|
||||
guard !didBuildUI else {
|
||||
if currentDisplayType == .pageCurl {
|
||||
@@ -606,21 +510,80 @@ public class RDReaderView: UIView {
|
||||
preloadController.initializeSignature(preloadEnvironment)
|
||||
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
// 不取消底层触摸事件,确保工具栏按钮(返回等)的 touchUpInside 能正常触发
|
||||
|
||||
tapGestureRecognizer.cancelsTouchesInView = false
|
||||
|
||||
}
|
||||
|
||||
/// 点击手势响应方法
|
||||
/// 根据点击位置将屏幕三等分,判断点击区域并设置 tapEvent
|
||||
@objc private func tapAction(tap: UITapGestureRecognizer) {
|
||||
let point = tap.location(in: tap.view)
|
||||
let hitView = hitTest(point, with: nil)
|
||||
if containsTextContentView(in: hitView) {
|
||||
return
|
||||
}
|
||||
if shouldSuppressChromeToggle(for: hitView, point: point) {
|
||||
return
|
||||
}
|
||||
handleResolvedTap(at: point, hitView: hitView, in: tap.view)
|
||||
}
|
||||
|
||||
private func shouldSuppressChromeToggle(for hitView: UIView?, point: CGPoint) -> Bool {
|
||||
if selectionTapSuppressedContentViews.allObjects.isEmpty == false {
|
||||
return true
|
||||
}
|
||||
var currentView = hitView
|
||||
while let view = currentView {
|
||||
if let textContentView = view as? RDEPUBTextContentView {
|
||||
let localPoint = convert(point, to: textContentView)
|
||||
return textContentView.shouldSuppressReaderTap(at: localPoint)
|
||||
}
|
||||
currentView = view.superview
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func registerSelectionGestureDependenciesIfNeeded(for contentView: UIView) {
|
||||
guard let textContentView = contentView as? RDEPUBTextContentView else { return }
|
||||
let longPressGesture = textContentView.selectionLongPressGestureRecognizer
|
||||
if registeredSelectionLongPressRecognizers.allObjects.contains(where: { $0 === longPressGesture }) {
|
||||
return
|
||||
}
|
||||
tapGestureRecognizer.require(toFail: longPressGesture)
|
||||
registeredSelectionLongPressRecognizers.add(longPressGesture)
|
||||
}
|
||||
|
||||
func updateSelectionTapSuppression(for contentView: UIView, isSuppressed: Bool) {
|
||||
if isSuppressed {
|
||||
if selectionTapSuppressedContentViews.allObjects.contains(where: { $0 === contentView }) == false {
|
||||
selectionTapSuppressedContentViews.add(contentView)
|
||||
}
|
||||
} else {
|
||||
selectionTapSuppressedContentViews.remove(contentView)
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelectionPagingSuppression(for contentView: UIView, isSuppressed: Bool) {
|
||||
if isSuppressed {
|
||||
if selectionPagingSuppressedContentViews.allObjects.contains(where: { $0 === contentView }) == false {
|
||||
selectionPagingSuppressedContentViews.add(contentView)
|
||||
}
|
||||
} else {
|
||||
selectionPagingSuppressedContentViews.remove(contentView)
|
||||
}
|
||||
updatePagingInteractionSuppression()
|
||||
}
|
||||
|
||||
func handleContentTap(at point: CGPoint, in sourceView: UIView) {
|
||||
let localPoint = sourceView.convert(point, to: self)
|
||||
handleResolvedTap(at: localPoint, hitView: nil, in: self)
|
||||
}
|
||||
|
||||
private func handleResolvedTap(at point: CGPoint, hitView: UIView?, in tapView: UIView?) {
|
||||
if isShowToolView {
|
||||
let hitView = hitTest(point, with: nil)
|
||||
if let top = topToolView, isHitView(hitView, inside: top, point: point) { return }
|
||||
if let bottom = bottomToolView, isHitView(hitView, inside: bottom, point: point) { return }
|
||||
}
|
||||
guard let viewFrame = tap.view?.frame else { return }
|
||||
guard let viewFrame = tapView?.frame else { return }
|
||||
tapEvent = tapRegionHandler.resolveTapEvent(
|
||||
point: point,
|
||||
viewFrame: viewFrame,
|
||||
@@ -628,9 +591,29 @@ public class RDReaderView: UIView {
|
||||
)
|
||||
}
|
||||
|
||||
/// 切换翻页模式(仿真/水平滚动/上下滚动)
|
||||
/// 会重建底层视图(PageViewController 或 CollectionView),并恢复到当前页
|
||||
/// - Parameter displayType: 目标翻页模式
|
||||
private func containsTextContentView(in hitView: UIView?) -> Bool {
|
||||
var currentView = hitView
|
||||
while let view = currentView {
|
||||
if view is RDEPUBTextContentView {
|
||||
return true
|
||||
}
|
||||
currentView = view.superview
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func updatePagingInteractionSuppression() {
|
||||
let shouldSuppress = selectionPagingSuppressedContentViews.allObjects.isEmpty == false
|
||||
guard shouldSuppress != isPagingInteractionSuppressed else { return }
|
||||
isPagingInteractionSuppressed = shouldSuppress
|
||||
setPagingInteractionEnabled(!shouldSuppress)
|
||||
}
|
||||
|
||||
private func setPagingInteractionEnabled(_ isEnabled: Bool) {
|
||||
collectionView.isScrollEnabled = isEnabled
|
||||
pageViewController.gestureRecognizers.forEach { $0.isEnabled = isEnabled }
|
||||
}
|
||||
|
||||
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType) {
|
||||
let previousDisplayType = currentDisplayType
|
||||
self.currentDisplayType = displayType
|
||||
@@ -640,7 +623,7 @@ public class RDReaderView: UIView {
|
||||
if previousDisplayType != displayType {
|
||||
invalidatePageCaches()
|
||||
}
|
||||
// 同步横屏双页标记到布局
|
||||
|
||||
layout.isLandscapeDualPage = landscapeDualPageEnabled && isLandscape
|
||||
layout.coverPageIndex = coverPageIndex
|
||||
switch displayType {
|
||||
@@ -653,13 +636,13 @@ public class RDReaderView: UIView {
|
||||
primePageCache(around: currentPage, preferredForward: predictedPageDirection)
|
||||
default:
|
||||
detachPageViewControllerIfNeeded()
|
||||
// RTL 水平模式翻转 collectionView
|
||||
|
||||
if pageDirection == .rightToLeft && displayType != .verticalScroll {
|
||||
collectionView.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
} else {
|
||||
collectionView.transform = .identity
|
||||
}
|
||||
// 确保 collectionView frame 正确后再触发布局计算
|
||||
|
||||
collectionView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
|
||||
insertSubview(self.collectionView, at: 0)
|
||||
layout.displayType = displayType
|
||||
@@ -668,13 +651,6 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 跳转到指定页码
|
||||
/// 支持所有翻页模式:
|
||||
/// - pageCurl:通过 UIPageViewController.setViewControllers 实现
|
||||
/// - 滚动模式:通过 setContentOffset 实现
|
||||
/// - Parameters:
|
||||
/// - pageNum: 目标页码(item 索引)
|
||||
/// - animated: 是否动画过渡
|
||||
public func transitionToPage(pageNum: Int, animated: Bool = false) {
|
||||
guard let safePageNum = clampedPageNumber(pageNum) else { return }
|
||||
switch currentDisplayType {
|
||||
@@ -685,7 +661,7 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
|
||||
let isDualPage = landscapeDualPageEnabled && isLandscape
|
||||
// RTL 时动画方向需要反转
|
||||
|
||||
let direction: UIPageViewController.NavigationDirection
|
||||
if pageDirection == .rightToLeft {
|
||||
direction = safePageNum > currentPage ? .reverse : .forward
|
||||
@@ -707,7 +683,7 @@ public class RDReaderView: UIView {
|
||||
self.finishPageCurlTransition()
|
||||
}
|
||||
} else {
|
||||
// 封面页独占或奇数最后一页:右侧放空白页
|
||||
|
||||
let blankNum = isFullScreenPage(pair.left) ? RDReaderView.blankPageNum : RDReaderView.blankEndPageNum
|
||||
let emptyVC = RDReaderPageChildViewController(contentView: UIView(), pageNum: blankNum)
|
||||
pageViewController.setViewControllers([leftVC, emptyVC], direction: animated ? direction : .forward, animated: animated) { [weak self] _ in
|
||||
@@ -738,22 +714,18 @@ public class RDReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新加载数据
|
||||
/// 重新切换到当前翻页模式,并刷新工具栏
|
||||
public func reloadData() {
|
||||
switchReaderDisplayType(currentDisplayType)
|
||||
topToolView = resolvedTopChromeView()
|
||||
bottomToolView = resolvedBottomChromeView()
|
||||
}
|
||||
|
||||
/// 仅刷新总页数,不重建页面内容(用于后台元数据解析期间避免刷新掉用户选区)
|
||||
public func reloadPageCountOnly() {
|
||||
collectionView.reloadData()
|
||||
topToolView = resolvedTopChromeView()
|
||||
bottomToolView = resolvedBottomChromeView()
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
@@ -774,7 +746,14 @@ extension RDReaderView: UIGestureRecognizerDelegate {
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
guard gestureRecognizer === tapGestureRecognizer else { return true }
|
||||
|
||||
if selectionTapSuppressedContentViews.allObjects.isEmpty == false {
|
||||
return false
|
||||
}
|
||||
|
||||
let point = touch.location(in: self)
|
||||
if containsTextContentView(in: touch.view) {
|
||||
return false
|
||||
}
|
||||
if let topToolView, isHitView(touch.view, inside: topToolView, point: point) {
|
||||
return false
|
||||
}
|
||||
@@ -795,14 +774,10 @@ extension RDReaderView: UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private var cellViewKey: Int8 = 0
|
||||
|
||||
/// UIView 扩展:通过响应链查找最近的父 UIViewController
|
||||
extension UIView {
|
||||
/// 沿响应链向上查找最近的 UIViewController
|
||||
/// 用于在视图中获取其所在控制器的引用
|
||||
|
||||
var ss_superViewController: UIViewController? {
|
||||
var next = self.next
|
||||
while next != nil {
|
||||
|
||||
@@ -1,91 +1,69 @@
|
||||
//
|
||||
// RDReaderViewProtocols.swift
|
||||
// ReadViewSDK
|
||||
//
|
||||
// 文件职责:阅读器核心协议定义,包括数据源、代理、页面提供者、页面导航等接口。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 阅读器数据源协议
|
||||
/// 上层控制器通过实现此协议,向 RDReaderView 提供页面数量、内容视图和工具栏。
|
||||
/// 所有方法由 RDReaderView 在需要渲染页面时回调。
|
||||
@objc public protocol RDReaderDataSource: NSObjectProtocol {
|
||||
/// 返回阅读器的总页数
|
||||
|
||||
func pageCountOfReaderView(readerView: RDReaderView) -> Int
|
||||
/// 返回指定页码的内容视图
|
||||
|
||||
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
||||
/// 返回指定页码的唯一标识符,用于 UICollectionViewCell 复用
|
||||
|
||||
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
|
||||
/// 返回顶部工具栏视图(可选),点击屏幕中央时会显示/隐藏
|
||||
|
||||
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
|
||||
/// 返回底部工具栏视图(可选),点击屏幕中央时会显示/隐藏
|
||||
|
||||
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
|
||||
}
|
||||
|
||||
/// 与内容格式无关的统一分页提供者协议。
|
||||
/// 逐步替代面向 EPUB 命名的 ``RDReaderDataSource``,方便后续复用到 PDF 等其他阅读场景。
|
||||
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
|
||||
/// 返回阅读器的总页数
|
||||
|
||||
func numberOfPages(in readerView: RDReaderView) -> Int
|
||||
/// 返回指定页码的内容视图,支持通过 reusableView 复用已有视图
|
||||
|
||||
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
||||
/// 返回指定页码的唯一标识符,用于 UICollectionViewCell 复用
|
||||
|
||||
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
|
||||
/// 返回顶部工具栏视图(可选)
|
||||
|
||||
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
|
||||
/// 返回底部工具栏视图(可选)
|
||||
|
||||
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
|
||||
}
|
||||
|
||||
/// 阅读器代理协议
|
||||
/// 上层控制器通过实现此协议,接收翻页和横竖屏切换事件
|
||||
@objc public protocol RDReaderDelegate: NSObjectProtocol {
|
||||
/// 翻页回调,当当前显示页码变化时触发
|
||||
|
||||
func pageNum(readerView: RDReaderView, pageNum: Int)
|
||||
/// 横竖屏切换时回调,可在此重新分页
|
||||
|
||||
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
|
||||
}
|
||||
|
||||
/// 阅读器页面导航协议
|
||||
/// 提供与具体实现无关的页面导航接口,外部可通过此协议控制翻页和刷新。
|
||||
public protocol RDReaderPageNavigating: AnyObject {
|
||||
/// 当前显示的页码
|
||||
|
||||
var currentPage: Int { get }
|
||||
/// 重新加载所有页面数据
|
||||
|
||||
func reloadPages()
|
||||
/// 跳转到指定页码,可选是否带动画
|
||||
|
||||
func transition(to page: Int, animated: Bool)
|
||||
}
|
||||
|
||||
// MARK: - 枚举
|
||||
|
||||
extension RDReaderView {
|
||||
/// 翻页模式枚举,决定 RDReaderView 使用哪种底层视图来展示内容
|
||||
|
||||
public enum DisplayType {
|
||||
/// 仿真翻页:使用 UIPageViewController,模拟真实翻书效果
|
||||
|
||||
case pageCurl
|
||||
/// 水平滚动:使用 UICollectionView + 自定义布局,每屏一项,水平分页
|
||||
|
||||
case horizontalScroll
|
||||
/// 垂直滚动:使用 UICollectionView,全宽项目,垂直连续滚动
|
||||
|
||||
case verticalScroll
|
||||
}
|
||||
|
||||
/// 翻页方向枚举
|
||||
public enum PageDirection {
|
||||
/// 从左往右翻页(默认,适用于中文/英文书籍)
|
||||
|
||||
case leftToRight
|
||||
/// 从右往左翻页(适用于日文漫画等)
|
||||
|
||||
case rightToLeft
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Legacy 适配器
|
||||
|
||||
/// 旧版数据源适配器
|
||||
/// 将 ``RDReaderDataSource`` 协议适配为 ``RDReaderPageProvider``,实现渐进式迁移。
|
||||
final class RDReaderLegacyDataSourceAdapter: NSObject, RDReaderPageProvider {
|
||||
/// 被适配的旧版数据源
|
||||
|
||||
weak var dataSource: RDReaderDataSource?
|
||||
|
||||
func numberOfPages(in readerView: RDReaderView) -> Int {
|
||||
|
||||
Reference in New Issue
Block a user