fix epub reader playback and trial navigation
This commit is contained in:
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user