fix epub reader playback and trial navigation
This commit is contained in:
@@ -49,14 +49,24 @@ final class RDEPUBReaderChapterListController: UITableViewController {
|
||||
cell.backgroundColor = theme.contentBackgroundColor
|
||||
cell.textLabel?.numberOfLines = 2
|
||||
cell.textLabel?.text = item.title
|
||||
cell.textLabel?.textColor = isCurrentItem(item) ? .systemBlue : theme.contentTextColor
|
||||
if isCurrentItem(item) {
|
||||
cell.textLabel?.textColor = .systemBlue
|
||||
} else if item.isReadable {
|
||||
cell.textLabel?.textColor = theme.contentTextColor
|
||||
} else {
|
||||
cell.textLabel?.textColor = .systemGray
|
||||
}
|
||||
cell.selectionStyle = item.isReadable ? .default : .none
|
||||
cell.isUserInteractionEnabled = item.isReadable
|
||||
cell.indentationLevel = item.depth
|
||||
cell.indentationWidth = 18
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
onSelectItem?(items[indexPath.row])
|
||||
let item = items[indexPath.row]
|
||||
guard item.isReadable else { return }
|
||||
onSelectItem?(item)
|
||||
}
|
||||
|
||||
private func isCurrentItem(_ item: RDEPUBReaderTableOfContentsItem) -> Bool {
|
||||
|
||||
@@ -15,6 +15,18 @@ extension RDEPUBReaderController: RDEpubReaderDataSource, RDEpubReaderPageProvid
|
||||
pageIdentifier(readerView: readerView, pageNum: index)
|
||||
}
|
||||
|
||||
public func readerView(
|
||||
_ readerView: RDEpubReaderView,
|
||||
shouldPreloadPageAt index: Int
|
||||
) -> Bool {
|
||||
// 试读墙由宿主提供唯一 UIView,不允许预加载创建另一容器并抢走它。
|
||||
guard index != trialWallPageIndex else { return false }
|
||||
// 交互型固定版式页会在文档加载阶段触发页内音频/时间轴。
|
||||
// 预加载会把相邻章节一并载入,导致当前章打开时多段音频同时播放。
|
||||
// 这些页面只在真正展示时再创建,确保仅当前查看章节发声。
|
||||
return request(for: index)?.isFixedLayout != true
|
||||
}
|
||||
|
||||
public func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView? {
|
||||
topToolView(readerView: readerView)
|
||||
}
|
||||
@@ -66,6 +78,7 @@ extension RDEPUBReaderController: RDEpubReaderDataSource, RDEpubReaderPageProvid
|
||||
pageNumber: pageNum + 1,
|
||||
totalPages: pageCountOfReaderView(readerView: readerView),
|
||||
configuration: configuration,
|
||||
contentInsets: readerContext.currentTextContentInsets(pageIndex: pageNum),
|
||||
chapterCFIMap: resolvedPage.chapter.chapterOffsetMap.cfiMap,
|
||||
chapterFragmentOffsets: resolvedPage.chapter.chapterOffsetMap.fragmentOffsets,
|
||||
highlights: textHighlights(for: resolvedPage.page),
|
||||
@@ -89,7 +102,8 @@ extension RDEPUBReaderController: RDEpubReaderDataSource, RDEpubReaderPageProvid
|
||||
contentView.configureLoading(
|
||||
pageNumber: pageNum + 1,
|
||||
totalPages: pageCountOfReaderView(readerView: readerView),
|
||||
configuration: configuration
|
||||
configuration: configuration,
|
||||
contentInsets: readerContext.currentTextContentInsets(pageIndex: pageNum)
|
||||
)
|
||||
return contentView
|
||||
}
|
||||
@@ -111,6 +125,7 @@ extension RDEPUBReaderController: RDEpubReaderDataSource, RDEpubReaderPageProvid
|
||||
pageNumber: pageNum + 1,
|
||||
totalPages: textBook.pages.count,
|
||||
configuration: configuration,
|
||||
contentInsets: readerContext.currentTextContentInsets(pageIndex: pageNum),
|
||||
chapterCFIMap: textBook.chapterData(for: page.href)?.chapter.cfiMap,
|
||||
chapterFragmentOffsets: textBook.chapterData(for: page.href)?.chapter.fragmentOffsets ?? [:],
|
||||
highlights: textHighlights(for: page),
|
||||
|
||||
@@ -48,12 +48,7 @@ extension RDEPUBReaderController {
|
||||
forPageNumber: pageIndex + 1,
|
||||
spineIndex: page.spineIndex
|
||||
)
|
||||
var preferences = currentPreferences()
|
||||
if publication.layout == .fixed, readerView.pagesPerScreen == 2 {
|
||||
// 横屏双页:左右页各让出 5pt,拼出 10pt 的书脊间距
|
||||
preferences.fixedContentInset.left += 5
|
||||
preferences.fixedContentInset.right += 5
|
||||
}
|
||||
let preferences = readerContext.currentPreferences(pageIndex: pageIndex)
|
||||
return preferences.renderRequest(
|
||||
for: page,
|
||||
publication: publication,
|
||||
|
||||
@@ -50,12 +50,14 @@ extension RDEPUBReaderController {
|
||||
items.flatMap { item in
|
||||
let location = RDEPUBLocation(bookIdentifier: currentBookIdentifier, href: item.href, progression: 0)
|
||||
let pageNumber = includePageNumbers ? resolvedTableOfContentsPageNumber(for: location) : nil
|
||||
let isReadable = isReadableTableOfContentsLocation(location)
|
||||
|
||||
let current = RDEPUBReaderTableOfContentsItem(
|
||||
title: item.title,
|
||||
href: item.href,
|
||||
depth: depth,
|
||||
pageNumber: pageNumber
|
||||
pageNumber: pageNumber,
|
||||
isReadable: isReadable
|
||||
)
|
||||
return [current] + flattenedTableOfContentsItems(
|
||||
from: item.children,
|
||||
|
||||
@@ -56,6 +56,17 @@ extension RDEPUBReaderController {
|
||||
return readableContentPageCount + 1
|
||||
}
|
||||
|
||||
func isReadableTableOfContentsLocation(_ location: RDEPUBLocation) -> Bool {
|
||||
guard let policy = configuration.trialPolicy,
|
||||
let readableChapterCount = policy.readableChapterCount else {
|
||||
return true
|
||||
}
|
||||
guard let spineIndex = readerContext.normalizedSpineIndex(for: location) else {
|
||||
return true
|
||||
}
|
||||
return spineIndex < readableChapterCount
|
||||
}
|
||||
|
||||
/// 构建试读墙页承载视图(铺满一页,居中放置宿主提供的墙视图)
|
||||
func makeTrialWallPageView(reusableView: UIView?) -> UIView {
|
||||
let container = (reusableView as? RDEPUBTrialWallContainerView) ?? RDEPUBTrialWallContainerView()
|
||||
@@ -65,12 +76,19 @@ extension RDEPUBReaderController {
|
||||
}
|
||||
|
||||
/// 试读墙页容器:让宿主墙视图铺满整页
|
||||
final class RDEPUBTrialWallContainerView: UIView {
|
||||
final class RDEPUBTrialWallContainerView: UIView, RDEpubReaderCachePolicyProviding {
|
||||
|
||||
/// 宿主只提供一份试读墙 View。预加载会同时构建多个页容器,若缓存该容器会
|
||||
/// 把同一 View 从实际可见页移走,最终表现为横竖翻页的试读墙白屏。
|
||||
var shouldAvoidReaderPageCaching: Bool { true }
|
||||
|
||||
private weak var wallView: UIView?
|
||||
|
||||
func setWallView(_ view: UIView?) {
|
||||
guard wallView !== view else { return }
|
||||
// 即使 View 对象相同,只要它已被其他复用容器接管,也必须重新挂回当前页。
|
||||
if wallView === view, view?.superview === self {
|
||||
return
|
||||
}
|
||||
wallView?.removeFromSuperview()
|
||||
guard let view else { return }
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
@@ -149,6 +149,10 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
/// 试读墙视图缓存(经 delegate 获取一次后复用;见 RDEPUBReaderController+Trial)
|
||||
var cachedTrialWallView: UIView?
|
||||
|
||||
/// 阅读器进入前宿主导航栏的真实状态。详情页使用自定义导航栏时系统栏原本
|
||||
/// 就是隐藏的,退出阅读器必须恢复该状态,不能无条件显示系统导航栏。
|
||||
private var navigationBarHiddenBeforeReader: Bool?
|
||||
|
||||
private(set) var isSearchBarVisible = false
|
||||
|
||||
var currentBookIdentifier: String? {
|
||||
@@ -275,6 +279,9 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
if navigationBarHiddenBeforeReader == nil {
|
||||
navigationBarHiddenBeforeReader = navigationController?.isNavigationBarHidden
|
||||
}
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
navigationController?.interactivePopGestureRecognizer?.delegate = self
|
||||
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
|
||||
@@ -282,7 +289,12 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
navigationController?.setNavigationBarHidden(false, animated: animated)
|
||||
if let navigationBarHiddenBeforeReader {
|
||||
navigationController?.setNavigationBarHidden(
|
||||
navigationBarHiddenBeforeReader,
|
||||
animated: animated
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
|
||||
@@ -10,10 +10,19 @@ public struct RDEPUBReaderTableOfContentsItem: Equatable {
|
||||
|
||||
public var pageNumber: Int?
|
||||
|
||||
public init(title: String, href: String, depth: Int, pageNumber: Int? = nil) {
|
||||
public var isReadable: Bool
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
href: String,
|
||||
depth: Int,
|
||||
pageNumber: Int? = nil,
|
||||
isReadable: Bool = true
|
||||
) {
|
||||
self.title = title
|
||||
self.href = href
|
||||
self.depth = depth
|
||||
self.pageNumber = pageNumber
|
||||
self.isReadable = isReadable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ protocol RDEPUBWebContentViewDelegate: AnyObject {
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didTapFootnoteWithAltText altText: String, sourceRect: CGRect?)
|
||||
}
|
||||
|
||||
final class RDEPUBWebContentView: UIView, RDEpubReaderPageResourceReleasing {
|
||||
final class RDEPUBWebContentView: UIView, RDEpubReaderPageResourceReleasing, RDEpubReaderPageVisibilityHandling {
|
||||
weak var delegate: RDEPUBWebContentViewDelegate?
|
||||
|
||||
/// The current href of the loaded chapter (exposed for image resolution).
|
||||
@@ -90,6 +90,10 @@ final class RDEPUBWebContentView: UIView, RDEpubReaderPageResourceReleasing {
|
||||
self?.decorationOverlayView.applyDecorations(decorations)
|
||||
}
|
||||
}
|
||||
|
||||
func readerPageVisibilityDidChange(isVisible: Bool) {
|
||||
epubWebView.setReaderPageVisible(isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
extension RDEPUBWebContentView: RDEPUBWebViewDelegate {
|
||||
|
||||
@@ -162,9 +162,12 @@ final class RDEPUBReaderContext {
|
||||
environment.currentLayoutContext(configuration: configuration)
|
||||
}
|
||||
|
||||
func currentPreferences() -> RDEPUBPreferences {
|
||||
let safeInsets = RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets)
|
||||
return configuration.makePreferences(safeAreaInsets: safeInsets)
|
||||
func currentPreferences(pageIndex: Int? = nil) -> RDEPUBPreferences {
|
||||
environment.currentPreferences(configuration: configuration, pageIndex: pageIndex)
|
||||
}
|
||||
|
||||
func currentTextContentInsets(pageIndex: Int) -> UIEdgeInsets {
|
||||
currentPreferences(pageIndex: pageIndex).reflowableContentInsets
|
||||
}
|
||||
|
||||
/// Captures all layout parameters needed for background chapter loading.
|
||||
|
||||
@@ -10,14 +10,59 @@ enum RDEPUBTextPageLayoutMetrics {
|
||||
|
||||
static func contentInsets(
|
||||
configuration: RDEPUBReaderConfiguration,
|
||||
safeAreaInsets: UIEdgeInsets
|
||||
safeAreaInsets: UIEdgeInsets,
|
||||
isLandscapeDualPage: Bool = false,
|
||||
isLeadingPage: Bool = true
|
||||
) -> UIEdgeInsets {
|
||||
let configInsets = configuration.reflowableContentInsets
|
||||
guard isLandscapeDualPage else {
|
||||
return UIEdgeInsets(
|
||||
top: max(configInsets.top, safeAreaInsets.top),
|
||||
left: max(configInsets.left, safeAreaInsets.left),
|
||||
bottom: max(configInsets.bottom, safeAreaInsets.bottom) + pageNumberReservedHeight,
|
||||
right: max(configInsets.right, safeAreaInsets.right)
|
||||
)
|
||||
}
|
||||
|
||||
let outerHorizontalInset = max(
|
||||
max(configInsets.left, configInsets.right),
|
||||
max(safeAreaInsets.left, safeAreaInsets.right)
|
||||
)
|
||||
let spineInset = RDEpubReaderView.landscapeDualPageGap / 2
|
||||
return UIEdgeInsets(
|
||||
top: max(configInsets.top, safeAreaInsets.top),
|
||||
left: max(configInsets.left, safeAreaInsets.left),
|
||||
bottom: max(configInsets.bottom, safeAreaInsets.bottom) + pageNumberReservedHeight,
|
||||
right: max(configInsets.right, safeAreaInsets.right)
|
||||
top: max(safeAreaInsets.top, min(configInsets.top, RDEPUBSafeArea.landscapeVerticalTextMargin)),
|
||||
left: isLeadingPage ? outerHorizontalInset : spineInset,
|
||||
bottom: max(safeAreaInsets.bottom, min(configInsets.bottom, RDEPUBSafeArea.landscapeVerticalTextMargin)) + pageNumberReservedHeight,
|
||||
right: isLeadingPage ? spineInset : outerHorizontalInset
|
||||
)
|
||||
}
|
||||
|
||||
static func fixedContentInsets(
|
||||
configuration: RDEPUBReaderConfiguration,
|
||||
safeAreaInsets: UIEdgeInsets,
|
||||
isLandscapeDualPage: Bool,
|
||||
isLeadingPage: Bool
|
||||
) -> UIEdgeInsets {
|
||||
let configInsets = configuration.fixedContentInset
|
||||
guard isLandscapeDualPage else {
|
||||
return UIEdgeInsets(
|
||||
top: max(configInsets.top, safeAreaInsets.top),
|
||||
left: max(configInsets.left, safeAreaInsets.left),
|
||||
bottom: max(configInsets.bottom, safeAreaInsets.bottom),
|
||||
right: max(configInsets.right, safeAreaInsets.right)
|
||||
)
|
||||
}
|
||||
|
||||
let outerHorizontalInset = max(
|
||||
max(configInsets.left, configInsets.right),
|
||||
max(safeAreaInsets.left, safeAreaInsets.right)
|
||||
)
|
||||
let spineInset = RDEpubReaderView.landscapeDualPageGap / 2
|
||||
return UIEdgeInsets(
|
||||
top: max(safeAreaInsets.top, min(configInsets.top, RDEPUBSafeArea.landscapeVerticalTextMargin)),
|
||||
left: isLeadingPage ? outerHorizontalInset : spineInset,
|
||||
bottom: max(safeAreaInsets.bottom, min(configInsets.bottom, RDEPUBSafeArea.landscapeVerticalTextMargin)),
|
||||
right: isLeadingPage ? spineInset : outerHorizontalInset
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,7 +119,8 @@ final class RDEPUBReaderEnvironment {
|
||||
frameHeight: max(pageSize.height, 1),
|
||||
edgeInsets: RDEPUBTextPageLayoutMetrics.contentInsets(
|
||||
configuration: configuration,
|
||||
safeAreaInsets: safeAreaInsets
|
||||
safeAreaInsets: safeAreaInsets,
|
||||
isLandscapeDualPage: readerView?.pagesPerScreen == 2
|
||||
),
|
||||
numberOfColumns: configuration.numberOfColumns,
|
||||
columnGap: configuration.columnGap,
|
||||
@@ -87,6 +133,29 @@ final class RDEPUBReaderEnvironment {
|
||||
)
|
||||
}
|
||||
|
||||
func currentPreferences(
|
||||
configuration: RDEPUBReaderConfiguration,
|
||||
pageIndex: Int? = nil
|
||||
) -> RDEPUBPreferences {
|
||||
let safeAreaInsets = RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets)
|
||||
let isLandscapeDualPage = readerView?.pagesPerScreen == 2
|
||||
let isLeadingPage = pageIndex.map { readerView?.isLeadingPage($0) ?? true } ?? true
|
||||
var preferences = configuration.makePreferences(safeAreaInsets: safeAreaInsets)
|
||||
preferences.reflowableContentInsets = RDEPUBTextPageLayoutMetrics.contentInsets(
|
||||
configuration: configuration,
|
||||
safeAreaInsets: safeAreaInsets,
|
||||
isLandscapeDualPage: isLandscapeDualPage,
|
||||
isLeadingPage: isLeadingPage
|
||||
)
|
||||
preferences.fixedContentInset = RDEPUBTextPageLayoutMetrics.fixedContentInsets(
|
||||
configuration: configuration,
|
||||
safeAreaInsets: safeAreaInsets,
|
||||
isLandscapeDualPage: isLandscapeDualPage,
|
||||
isLeadingPage: isLeadingPage
|
||||
)
|
||||
return preferences
|
||||
}
|
||||
|
||||
var currentBrightness: CGFloat {
|
||||
get { displayEnvironment.currentBrightness }
|
||||
set { displayEnvironment.currentBrightness = newValue }
|
||||
|
||||
+4
-1
@@ -297,6 +297,9 @@ final class RDEPUBReaderSearchCoordinator {
|
||||
private func notifySearchStateChanged() {
|
||||
guard let controller else { return }
|
||||
let state = controller.searchState
|
||||
// 搜索在后台队列执行,结果落地后需要同步刷新搜索面板计数;
|
||||
// 否则正文高亮已更新时,列表仍可能停留在“搜索中”或空状态。
|
||||
controller.updateSearchCount()
|
||||
controller.delegate?.epubReader(controller, didUpdateSearchResult: state?.result)
|
||||
controller.delegate?.epubReader(controller, didChangeCurrentSearchMatch: state?.currentMatch)
|
||||
}
|
||||
@@ -411,4 +414,4 @@ final class RDEPUBReaderSearchCoordinator {
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,6 +398,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDEpubRe
|
||||
pageNumber: Int,
|
||||
totalPages: Int,
|
||||
configuration: RDEPUBReaderConfiguration,
|
||||
contentInsets: UIEdgeInsets? = nil,
|
||||
chapterCFIMap: RDEPUBCFIMap? = nil,
|
||||
chapterFragmentOffsets: [String: Int] = [:],
|
||||
highlights: [RDEPUBHighlight] = [],
|
||||
@@ -414,7 +415,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDEpubRe
|
||||
currentHighlights = highlights
|
||||
currentSearchState = searchState
|
||||
selectionController.clearSelection(renderView: coreTextRenderView)
|
||||
contentInsets = Self.safeContentInsets(
|
||||
self.contentInsets = contentInsets ?? Self.safeContentInsets(
|
||||
configuration: configuration,
|
||||
safeAreaInsets: RDEPUBSafeArea.resolve(safeAreaInsets)
|
||||
)
|
||||
@@ -491,7 +492,8 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDEpubRe
|
||||
func configureLoading(
|
||||
pageNumber: Int,
|
||||
totalPages: Int,
|
||||
configuration: RDEPUBReaderConfiguration
|
||||
configuration: RDEPUBReaderConfiguration,
|
||||
contentInsets: UIEdgeInsets? = nil
|
||||
) {
|
||||
currentPage = nil
|
||||
currentChapterCFIMap = nil
|
||||
@@ -502,7 +504,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDEpubRe
|
||||
currentHighlights = []
|
||||
currentSearchState = nil
|
||||
selectionController.clearSelection(renderView: coreTextRenderView)
|
||||
contentInsets = Self.safeContentInsets(
|
||||
self.contentInsets = contentInsets ?? Self.safeContentInsets(
|
||||
configuration: configuration,
|
||||
safeAreaInsets: RDEPUBSafeArea.resolve(safeAreaInsets)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user