fix epub reader playback and trial navigation

This commit is contained in:
shen
2026-07-11 08:16:56 +08:00
parent d7fcda345d
commit 063a493b18
231 changed files with 435 additions and 36060 deletions
@@ -12,6 +12,12 @@ protocol RDEpubReaderPageResourceReleasing {
func releaseResources()
}
///
///
protocol RDEpubReaderPageVisibilityHandling: AnyObject {
func readerPageVisibilityDidChange(isVisible: Bool)
}
final class RDEpubReaderPreloadController {
var radius: Int = 2
@@ -121,6 +127,17 @@ final class RDEpubReaderPreloadController {
return preloaded
}
func updatePageVisibility(visiblePageNumbers: Set<Int>) {
var notifiedViews = Set<ObjectIdentifier>()
let cachedViews = pageCurlCachedViews.merging(preloadedPageViews) { current, _ in current }
for (pageNumber, view) in cachedViews {
let identifier = ObjectIdentifier(view)
guard notifiedViews.insert(identifier).inserted else { continue }
(view as? RDEpubReaderPageVisibilityHandling)?
.readerPageVisibilityDidChange(isVisible: visiblePageNumbers.contains(pageNumber))
}
}
func prime(
around pageNum: Int,
preferredForward: Bool? = nil,
@@ -164,7 +181,14 @@ final class RDEpubReaderPreloadController {
"cache HIT(pageCurl) page=\(targetPage) view=\(RDEpubReaderTapDebug.describe(cached))"
)
} else {
contentView = contentViewProvider(targetPage, nil) ?? UIView()
guard let providedView = contentViewProvider(targetPage, nil) else {
//
//
preloadedPageViews.removeValue(forKey: targetPage)
pageCurlCachedViews.removeValue(forKey: targetPage)
continue
}
contentView = providedView
RDEpubReaderTapDebug.log(
"PreloadController.prime",
"cache MISS page=\(targetPage) created=\(RDEpubReaderTapDebug.describe(contentView))"
@@ -9,7 +9,10 @@ extension RDEpubReaderView: UICollectionViewDataSource, RDEpubReaderFlowLayoutDe
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifer, for: IndexPath(item: indexPath.row, section: 0)) as! RDEpubReaderContentCell
let preloadedView = preloadController.takePreloadedView(for: indexPath.row)
let reusableView = cell.containerView ?? preloadedView
cell.containerView = contentViewForPage(indexPath.row, reusableView: reusableView)
let contentView = contentViewForPage(indexPath.row, reusableView: reusableView)
(contentView as? RDEpubReaderPageVisibilityHandling)?
.readerPageVisibilityDidChange(isVisible: isReaderPageVisible(indexPath.row))
cell.containerView = contentView
if pageDirection == .rightToLeft && currentDisplayType != .verticalScroll {
cell.contentView.transform = CGAffineTransform(scaleX: -1, y: 1)
@@ -50,6 +50,10 @@ enum RDEpubReaderTapDebug {
public class RDEpubReaderView: UIView {
/// The readable-content gutter between two landscape pages. Each page
/// contributes half of this value from its inner content inset.
public static let landscapeDualPageGap: CGFloat = 10
enum TapEvent {
case none
@@ -93,6 +97,9 @@ public class RDEpubReaderView: UIView {
public var currentPage: Int = -1 {
didSet {
if currentPage != oldValue {
updatePageContentVisibility()
}
if let delegate = delegate, currentPage != oldValue, delegate.responds(to: #selector(RDEpubReaderDelegate.pageNum(readerView:pageNum:))) {
delegate.pageNum(readerView: self, pageNum: currentPage)
}
@@ -233,6 +240,13 @@ public class RDEpubReaderView: UIView {
)
}
/// Whether a page occupies the leading (left in LTR) side of a landscape
/// spread. Cover pages remain full-screen and are treated as leading.
public func isLeadingPage(_ pageNum: Int) -> Bool {
guard pagesPerScreen > 1, !isFullScreenPage(pageNum) else { return true }
return dualPagePair(for: pageNum).left == pageNum
}
private func dualPagePair(for pageNum: Int) -> (left: Int, right: Int?) {
let totalPages = numberOfPages()
return spreadResolver.dualPagePair(
@@ -380,6 +394,13 @@ public class RDEpubReaderView: UIView {
layout.isLandscapeDualPage = landscapeDualPageEnabled && isNowLandscape
layout.coverPageIndex = coverPageIndex
// Cached page views are configured with orientation-dependent content
// insets and Web render requests. Reusing a portrait-preloaded page in
// the rebuilt landscape page controller leaves the portrait safe-area
// inset on all four edges (and keeps fixed-layout Web content under the
// landscape sensor area). Drop them before requesting the new spread.
invalidatePageCaches()
switch currentDisplayType {
case .pageCurl:
@@ -503,11 +524,14 @@ public class RDEpubReaderView: UIView {
}
func pageViewForDisplay(pageNum: Int) -> UIView {
preloadController.pageViewForDisplay(
let pageView = preloadController.pageViewForDisplay(
pageNum: pageNum,
environment: preloadEnvironment,
contentViewProvider: pageContentViewForPreload(pageNum:reusableView:)
contentViewProvider: pageContentViewForDisplay(pageNum:reusableView:)
)
(pageView as? RDEpubReaderPageVisibilityHandling)?
.readerPageVisibilityDidChange(isVisible: isReaderPageVisible(pageNum))
return pageView
}
func primePageCache(around pageNum: Int, preferredForward: Bool? = nil) {
@@ -521,7 +545,50 @@ public class RDEpubReaderView: UIView {
}
private func pageContentViewForPreload(pageNum: Int, reusableView: UIView?) -> UIView? {
contentViewForPage(pageNum, reusableView: reusableView)
let allowsPreloading = resolvedPageProvider?
.readerView?(self, shouldPreloadPageAt: pageNum) ?? true
guard allowsPreloading else { return nil }
return contentViewForPage(pageNum, reusableView: reusableView)
}
private func pageContentViewForDisplay(pageNum: Int, reusableView: UIView?) -> UIView? {
return contentViewForPage(pageNum, reusableView: reusableView)
}
func isReaderPageVisible(_ pageNum: Int) -> Bool {
guard currentPage >= 0 else { return pageNum == 0 }
if landscapeDualPageEnabled && isLandscape {
let pair = dualPagePair(for: currentPage)
return pageNum == pair.left || pageNum == pair.right
}
return pageNum == currentPage
}
private func updatePageContentVisibility() {
var visiblePageNumbers = Set<Int>()
if currentPage >= 0 {
visiblePageNumbers.insert(currentPage)
if landscapeDualPageEnabled && isLandscape,
let rightPage = dualPagePair(for: currentPage).right {
visiblePageNumbers.insert(rightPage)
}
}
preloadController.updatePageVisibility(visiblePageNumbers: visiblePageNumbers)
for viewController in pageViewController.viewControllers ?? [] {
guard let pageController = viewController as? RDEpubReaderPageChildViewController,
let contentView = pageController.contentView else { continue }
(contentView as? RDEpubReaderPageVisibilityHandling)?
.readerPageVisibilityDidChange(isVisible: visiblePageNumbers.contains(pageController.pageNum))
}
for indexPath in collectionView.indexPathsForVisibleItems {
guard let cell = collectionView.cellForItem(at: indexPath) as? RDEpubReaderContentCell,
let contentView = cell.containerView else { continue }
(contentView as? RDEpubReaderPageVisibilityHandling)?
.readerPageVisibilityDidChange(isVisible: visiblePageNumbers.contains(indexPath.item))
}
}
public override func didMoveToSuperview() {
@@ -23,6 +23,10 @@ import UIKit
@objc optional func pageIdentifier(in readerView: RDEpubReaderView, index: Int) -> String?
///
/// 宿 View false
@objc optional func readerView(_ readerView: RDEpubReaderView, shouldPreloadPageAt index: Int) -> Bool
@objc optional func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView?
@objc optional func readerViewBottomChrome(_ readerView: RDEpubReaderView) -> UIView?