feat: 设置面板与章节运行时联动优化
- 设置页打开时仅重新计算当前章节预览,关闭后触发完整补全 - 新增 preview 加载优先级,支持设置页内当前章预览 - 添加设置页防抖机制,合并连续字号/行距变更 - 支持下滑手势关闭设置页,通过 presentation delegate 通知运行时 - 重新分页时跳过导航和尺寸校验,避免状态冲突 - currentTextPageSize 线程安全优化,后台线程使用缓存尺寸 - 更新 CocoaPods 依赖 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c64460988a
commit
6ed3fcb071
@ -43,7 +43,7 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
||||
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
||||
RDReaderView: 54205c82c62ea6c674b43c833642d2643c98c573
|
||||
RDReaderView: 617ec758a5db3c10024acf83b110465ea8343b5b
|
||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
@ -27,9 +27,15 @@
|
||||
"ZIPFoundation": [
|
||||
"~> 0.9"
|
||||
],
|
||||
"DTCoreText": [],
|
||||
"SnapKit": [],
|
||||
"SSAlertSwift": []
|
||||
"DTCoreText": [
|
||||
|
||||
],
|
||||
"SnapKit": [
|
||||
|
||||
],
|
||||
"SSAlertSwift": [
|
||||
|
||||
]
|
||||
},
|
||||
"requires_arc": true,
|
||||
"swift_version": "5.10"
|
||||
|
||||
2
ReadViewDemo/Pods/Manifest.lock
generated
2
ReadViewDemo/Pods/Manifest.lock
generated
@ -43,7 +43,7 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
||||
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
||||
RDReaderView: 54205c82c62ea6c674b43c833642d2643c98c573
|
||||
RDReaderView: 617ec758a5db3c10024acf83b110465ea8343b5b
|
||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||
SSAlertSwift: aad8dc0c20b36fcffe700b81d7be89d60c7ba4f1
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
2846
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
2846
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
@ -295,6 +295,9 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
updateCurrentSelection(nil)
|
||||
reconcileTextPaginationSizeIfNeeded(for: pageNum)
|
||||
|
||||
// 如果正在重新分页,跳过后续操作,避免状态冲突
|
||||
guard !isRepaginating else { return }
|
||||
|
||||
// 用户开始导航时,应用后台解析完成的完整 map
|
||||
let previousCurrentPage = readerView.currentPage
|
||||
runtime.applyPendingFullPageMapIfNeeded()
|
||||
@ -362,10 +365,17 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
}
|
||||
|
||||
isReconcilingTextPaginationSize = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
// 使用防抖延迟,避免连续快速点击字号时重复触发
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.isReconcilingTextPaginationSize = false
|
||||
guard (self.textBook != nil || self.readerContext.bookPageMap != nil), !self.isRepaginating else { return }
|
||||
// 再次检查尺寸是否仍然变化,避免过期的重新分页
|
||||
let currentSize = self.readerView.resolvedSinglePageSize(pageNum: self.readerView.currentPage)
|
||||
guard currentSize.width > 0, currentSize.height > 0 else { return }
|
||||
let stillChanged = abs(currentSize.width - self.lastTextPaginationPageSize!.width) > 0.5
|
||||
|| abs(currentSize.height - self.lastTextPaginationPageSize!.height) > 0.5
|
||||
guard stillChanged else { return }
|
||||
self.repaginatePreservingCurrentLocation()
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ final class RDEPUBChapterLoader {
|
||||
|
||||
enum LoadPriority {
|
||||
case navigation // 前台导航:用户主动跳章,完成后检查导航目标队列
|
||||
case preview // 设置预览:仅构建当前章并回调,不参与导航目标消费
|
||||
case prefetch // 后台预取:±1 相邻章,完成后仅回填缓存 + 刷新快照
|
||||
}
|
||||
|
||||
@ -101,6 +102,12 @@ final class RDEPUBChapterLoader {
|
||||
completion(.success(chapter))
|
||||
}
|
||||
|
||||
case .preview:
|
||||
store.markBuilding(false)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(chapter))
|
||||
}
|
||||
|
||||
case .prefetch:
|
||||
// 后台预取:仅回填缓存,标记预取目标完成
|
||||
// 不触发跳章,不检查导航目标队列
|
||||
|
||||
@ -8,7 +8,7 @@ import UIKit
|
||||
/// - 同步工具栏的主题和状态
|
||||
/// - 弹出设置面板和目录面板
|
||||
/// - 处理返回按钮的关闭逻辑
|
||||
final class RDEPUBReaderChromeCoordinator {
|
||||
final class RDEPUBReaderChromeCoordinator: NSObject, UIAdaptivePresentationControllerDelegate {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
init(context: RDEPUBReaderContext) {
|
||||
@ -19,6 +19,13 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
context.controller
|
||||
}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
// 设置页面被用户下滑关闭时
|
||||
context.runtime?.settingsPanelDidDisappear()
|
||||
}
|
||||
|
||||
/// 创建顶部工具栏视图,绑定返回、搜索和书签切换回调。
|
||||
func makeTopToolView() -> RDEPUBReaderTopToolView {
|
||||
let toolView = RDEPUBReaderTopToolView()
|
||||
@ -110,6 +117,10 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
func presentSettings() {
|
||||
guard let controller else { return }
|
||||
guard controller.configuration.showsSettingsPanel else { return }
|
||||
|
||||
// 通知 Runtime 设置页面即将打开
|
||||
context.runtime?.settingsPanelWillAppear()
|
||||
|
||||
let settingsController = RDEPUBReaderSettingsViewController(
|
||||
configuration: controller.configuration,
|
||||
brightness: controller.currentBrightness
|
||||
@ -135,9 +146,14 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
settingsController.onThemeChange = { [weak controller] theme in
|
||||
controller?.updateConfiguration { $0.theme = theme }
|
||||
}
|
||||
settingsController.onDismiss = { [weak self] in
|
||||
// 通知 Runtime 设置页面已关闭
|
||||
self?.context.runtime?.settingsPanelDidDisappear()
|
||||
}
|
||||
|
||||
let navigationController = UINavigationController(rootViewController: settingsController)
|
||||
navigationController.modalPresentationStyle = .pageSheet
|
||||
navigationController.presentationController?.delegate = self
|
||||
controller.present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
|
||||
@ -122,6 +122,7 @@ final class RDEPUBReaderContext {
|
||||
|
||||
/// 获取当前文本排版的单页尺寸,优先从 readerView 解析,兜底用布局上下文。
|
||||
func currentTextPageSize() -> CGSize {
|
||||
if Thread.isMainThread {
|
||||
let pageNum = (readerView?.currentPage ?? -1) >= 0 ? readerView?.currentPage : nil
|
||||
if let readerView, let pageNum {
|
||||
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
|
||||
@ -129,7 +130,23 @@ final class RDEPUBReaderContext {
|
||||
return resolvedSize
|
||||
}
|
||||
}
|
||||
return currentLayoutContext().viewportSize
|
||||
let viewportSize = currentLayoutContext().viewportSize
|
||||
if viewportSize.width > 0, viewportSize.height > 0 {
|
||||
return viewportSize
|
||||
}
|
||||
} else if let lastTextPaginationPageSize,
|
||||
lastTextPaginationPageSize.width > 0,
|
||||
lastTextPaginationPageSize.height > 0 {
|
||||
return lastTextPaginationPageSize
|
||||
} else {
|
||||
let mainThreadSize = DispatchQueue.main.sync { [weak self] in
|
||||
self?.currentTextPageSize() ?? .zero
|
||||
}
|
||||
if mainThreadSize.width > 0, mainThreadSize.height > 0 {
|
||||
return mainThreadSize
|
||||
}
|
||||
}
|
||||
return dependencies.environment.fallbackViewportSize
|
||||
}
|
||||
|
||||
/// 根据当前配置生成文本渲染样式(字体、行距、颜色)。
|
||||
|
||||
@ -27,6 +27,19 @@ final class RDEPUBReaderRuntime {
|
||||
lazy var backgroundCoverageStore = RDEPUBBackgroundCoverageStore(context: context)
|
||||
lazy var reconciliationCoordinator = RDEPUBPageMapReconciliationCoordinator(context: context)
|
||||
|
||||
/// 设置页面是否打开
|
||||
var isSettingsPanelOpen: Bool = false
|
||||
|
||||
/// 标记需要在设置页面关闭后触发完整补全
|
||||
var needsFullRepaginationAfterSettingsClose: Bool = false
|
||||
|
||||
/// 设置页内当前章预览的代次,用于丢弃过期结果
|
||||
private var settingsPreviewGeneration: Int = 0
|
||||
/// 设置页内 preview 防抖任务,只保留最后一次请求。
|
||||
private var pendingSettingsPreviewWorkItem: DispatchWorkItem?
|
||||
/// 设置页内 preview 防抖延迟,合并连续点击。
|
||||
private let settingsPreviewDebounceDelay: TimeInterval = 0.2
|
||||
|
||||
init(context: RDEPUBReaderContext) {
|
||||
self.context = context
|
||||
}
|
||||
@ -330,6 +343,9 @@ final class RDEPUBReaderRuntime {
|
||||
let readerView = context.readerView,
|
||||
let controller = context.controller else { return }
|
||||
|
||||
// 如果正在重新分页,跳过本次检查,避免状态冲突
|
||||
guard !controller.isRepaginating else { return }
|
||||
|
||||
// 使用协调器判断是否允许接管
|
||||
let decision = reconciliationCoordinator.evaluateTakeover(
|
||||
candidatePageMap: pendingMap,
|
||||
@ -401,8 +417,112 @@ final class RDEPUBReaderRuntime {
|
||||
|
||||
/// 重新分页并保持当前阅读位置不变
|
||||
func repaginatePreservingCurrentLocation() {
|
||||
// 如果设置页面打开,只计算当前章节(热区)
|
||||
if isSettingsPanelOpen {
|
||||
needsFullRepaginationAfterSettingsClose = true
|
||||
paginationCoordinator.cancelActiveMetadataParseWork()
|
||||
scheduleSettingsPreviewRepagination()
|
||||
} else {
|
||||
paginationCoordinator.repaginatePreservingCurrentLocation()
|
||||
}
|
||||
}
|
||||
|
||||
/// 防抖调度设置页内的当前章 preview,只保留最后一次请求。
|
||||
private func scheduleSettingsPreviewRepagination() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
settingsPreviewGeneration += 1
|
||||
let previewGeneration = settingsPreviewGeneration
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self,
|
||||
self.isSettingsPanelOpen,
|
||||
previewGeneration == self.settingsPreviewGeneration else {
|
||||
return
|
||||
}
|
||||
self.pendingSettingsPreviewWorkItem = nil
|
||||
self.chapterRuntimeStore.invalidateAllForSettingsChange()
|
||||
self.repaginateCurrentChapterOnly(previewGeneration: previewGeneration)
|
||||
}
|
||||
pendingSettingsPreviewWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(
|
||||
deadline: .now() + settingsPreviewDebounceDelay,
|
||||
execute: workItem
|
||||
)
|
||||
}
|
||||
|
||||
/// 只重新计算当前章节(设置页面打开时使用)
|
||||
private func repaginateCurrentChapterOnly(previewGeneration: Int) {
|
||||
guard let bookPageMap = context.bookPageMap,
|
||||
let readerView = context.readerView else { return }
|
||||
|
||||
let currentPageNumber = readerView.currentPage + 1
|
||||
guard let currentSpineIndex = bookPageMap.spineIndex(forAbsolutePage: currentPageNumber - 1) else {
|
||||
return
|
||||
}
|
||||
let previewLocation = locationCoordinator.currentVisibleLocation() ?? context.persistenceLocation()
|
||||
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Runtime",
|
||||
"repaginateCurrentChapterOnly spine=\(currentSpineIndex) generation=\(previewGeneration)"
|
||||
)
|
||||
|
||||
chapterLoader.loadChapter(
|
||||
spineIndex: currentSpineIndex,
|
||||
store: chapterRuntimeStore,
|
||||
priority: .preview
|
||||
) { [weak self] result in
|
||||
guard let self,
|
||||
self.isSettingsPanelOpen,
|
||||
previewGeneration == self.settingsPreviewGeneration,
|
||||
let readerView = self.context.readerView else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let chapter):
|
||||
let partialMap = self.makePartialPageMap(from: [chapter])
|
||||
self.context.bookPageMap = partialMap
|
||||
self.context.replaceActiveSnapshot(self.makeSnapshot(from: partialMap))
|
||||
readerView.reloadData()
|
||||
|
||||
if let previewLocation,
|
||||
self.locationCoordinator.restoreReadingLocation(previewLocation, animated: false) {
|
||||
return
|
||||
}
|
||||
|
||||
readerView.transitionToPage(pageNum: 0, animated: false)
|
||||
|
||||
case .failure(let error):
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Runtime",
|
||||
"repaginateCurrentChapterOnly FAILED spine=\(currentSpineIndex) generation=\(previewGeneration) error=\(error)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置页面即将打开
|
||||
func settingsPanelWillAppear() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
pendingSettingsPreviewWorkItem = nil
|
||||
isSettingsPanelOpen = true
|
||||
needsFullRepaginationAfterSettingsClose = false
|
||||
settingsPreviewGeneration += 1
|
||||
}
|
||||
|
||||
/// 设置页面已关闭
|
||||
func settingsPanelDidDisappear() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
pendingSettingsPreviewWorkItem = nil
|
||||
isSettingsPanelOpen = false
|
||||
settingsPreviewGeneration += 1
|
||||
// 如果在设置页面期间有配置变化,触发完整补全
|
||||
if needsFullRepaginationAfterSettingsClose {
|
||||
needsFullRepaginationAfterSettingsClose = false
|
||||
RDEPUBBackgroundTrace.log("Runtime", "settingsPanelDidDisappear: triggering full repagination")
|
||||
paginationCoordinator.repaginatePreservingCurrentLocation()
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新当前可见内容,保持阅读位置不变
|
||||
func refreshVisibleContentPreservingLocation() {
|
||||
|
||||
@ -22,6 +22,8 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
var onDisplayTypeChange: ((RDReaderView.DisplayType) -> Void)?
|
||||
/// 主题变化回调
|
||||
var onThemeChange: ((RDEPUBReaderTheme) -> Void)?
|
||||
/// 设置页面关闭回调
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
// MARK: 主题预设枚举
|
||||
|
||||
@ -292,7 +294,9 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
}
|
||||
|
||||
@objc private func doneAction() {
|
||||
dismiss(animated: true)
|
||||
dismiss(animated: true) { [weak self] in
|
||||
self?.onDismiss?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func brightnessChanged(_ slider: UISlider) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user