feat: improve epub reader controls and annotations
This commit is contained in:
@@ -127,14 +127,19 @@ final class RDEpubReaderPreloadController {
|
||||
return preloaded
|
||||
}
|
||||
|
||||
func updatePageVisibility(visiblePageNumbers: Set<Int>) {
|
||||
var notifiedViews = Set<ObjectIdentifier>()
|
||||
func updatePageVisibility(visibleViewIdentifiers: Set<ObjectIdentifier>) {
|
||||
let cachedViews = pageCurlCachedViews.merging(preloadedPageViews) { current, _ in current }
|
||||
for (pageNumber, view) in cachedViews {
|
||||
var notifiedViews = Set<ObjectIdentifier>()
|
||||
|
||||
// Cache entries are never the authority for activation: UIPageViewController
|
||||
// can create two views for the same page number. Keep every non-winning
|
||||
// instance suspended; the reader activates only its actually attached view.
|
||||
for view in cachedViews.values {
|
||||
let identifier = ObjectIdentifier(view)
|
||||
guard notifiedViews.insert(identifier).inserted else { continue }
|
||||
guard !visibleViewIdentifiers.contains(identifier),
|
||||
notifiedViews.insert(identifier).inserted else { continue }
|
||||
(view as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: visiblePageNumbers.contains(pageNumber))
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +152,7 @@ final class RDEpubReaderPreloadController {
|
||||
) {
|
||||
refreshCacheSignatureIfNeeded(environment)
|
||||
let targets = forecastTargets(around: pageNum, preferredForward: preferredForward, environment: environment)
|
||||
let keepSet = Set(targets).union(visiblePageNumbers(for: pageNum, environment: environment))
|
||||
let keepSet = Set(targets).union(spreadPageNumbers(startingAt: pageNum, environment: environment))
|
||||
trimCachedPageViews(keeping: keepSet)
|
||||
guard !targets.isEmpty else { return }
|
||||
|
||||
@@ -194,6 +199,8 @@ final class RDEpubReaderPreloadController {
|
||||
"cache MISS page=\(targetPage) created=\(RDEpubReaderTapDebug.describe(contentView))"
|
||||
)
|
||||
}
|
||||
(contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
let shouldCacheContentView = shouldCache(view: contentView, for: targetPage, environment: environment)
|
||||
if shouldCacheContentView {
|
||||
preloadedPageViews[targetPage] = contentView
|
||||
@@ -309,10 +316,6 @@ final class RDEpubReaderPreloadController {
|
||||
).left
|
||||
}
|
||||
|
||||
private func visiblePageNumbers(for anchorPage: Int, environment: Environment) -> Set<Int> {
|
||||
spreadPageNumbers(startingAt: anchorPage, environment: environment)
|
||||
}
|
||||
|
||||
private func detachedReusablePageView(for pageNum: Int) -> UIView? {
|
||||
if let preloaded = preloadedPageViews.removeValue(forKey: pageNum) {
|
||||
preloaded.removeFromSuperview()
|
||||
@@ -351,6 +354,8 @@ final class RDEpubReaderPreloadController {
|
||||
// 的缓存重新呈现,提前拆除会让翻页露出空白页。这类视图交由持有方
|
||||
// 释放后经 deinit 自然回收(弱代理已保证无保留环)。
|
||||
let isHeldOutsidePreloadHost = view.superview != nil && view.superview !== preloadHostView
|
||||
(view as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
view.removeFromSuperview()
|
||||
if !isHeldOutsidePreloadHost {
|
||||
(view as? RDEpubReaderPageResourceReleasing)?.releaseResources()
|
||||
|
||||
@@ -11,8 +11,11 @@ extension RDEpubReaderView: UICollectionViewDataSource, RDEpubReaderFlowLayoutDe
|
||||
let reusableView = cell.containerView ?? preloadedView
|
||||
let contentView = contentViewForPage(indexPath.row, reusableView: reusableView)
|
||||
(contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: isReaderPageVisible(indexPath.row))
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
cell.containerView = contentView
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.updatePageContentVisibility()
|
||||
}
|
||||
|
||||
if pageDirection == .rightToLeft && currentDisplayType != .verticalScroll {
|
||||
cell.contentView.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
|
||||
@@ -96,7 +96,28 @@ extension RDEpubReaderView: UIPageViewControllerDataSource, UIPageViewController
|
||||
}
|
||||
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||
let resolvedPage = (pageViewController.viewControllers?.first as? RDEpubReaderPageChildViewController)?.pageNum ?? -1
|
||||
let visibleControllers = pageViewController.viewControllers ?? []
|
||||
let visibleControllerIdentifiers = Set(visibleControllers.map(ObjectIdentifier.init))
|
||||
let transitionControllers = previousViewControllers + [
|
||||
willPreviousTransitionToViewController,
|
||||
willNextTransitionToViewController,
|
||||
willTransitionToViewController
|
||||
].compactMap { $0 }
|
||||
var hiddenContentViews = Set<ObjectIdentifier>()
|
||||
|
||||
// UIPageViewController can ask its data source for a second instance of
|
||||
// the same page and leave the first one outside our cache dictionaries.
|
||||
// Explicitly stop every controller that did not win the transition so
|
||||
// an orphaned Hype WebView cannot keep narrating in the background.
|
||||
for controller in transitionControllers where !visibleControllerIdentifiers.contains(ObjectIdentifier(controller)) {
|
||||
guard let childController = controller as? RDEpubReaderPageChildViewController,
|
||||
let contentView = childController.contentView else { continue }
|
||||
let identifier = ObjectIdentifier(contentView)
|
||||
guard hiddenContentViews.insert(identifier).inserted else { continue }
|
||||
(contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
}
|
||||
|
||||
if completed, let firstVC = pageViewController.viewControllers?.first as? RDEpubReaderPageChildViewController {
|
||||
let pn = firstVC.pageNum
|
||||
if pn != RDEpubReaderView.blankPageNum && pn != RDEpubReaderView.blankEndPageNum {
|
||||
@@ -105,6 +126,14 @@ extension RDEpubReaderView: UIPageViewControllerDataSource, UIPageViewController
|
||||
}
|
||||
}
|
||||
|
||||
// On cancellation currentPage does not change, so its didSet cannot
|
||||
// restore visibility. Reassert the actual controllers in both paths.
|
||||
for case let childController as RDEpubReaderPageChildViewController in visibleControllers {
|
||||
guard let contentView = childController.contentView else { continue }
|
||||
(contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: isReaderPageVisible(childController.pageNum))
|
||||
}
|
||||
|
||||
predictedPageDirection = nil
|
||||
finishPageCurlTransition()
|
||||
}
|
||||
|
||||
@@ -527,10 +527,13 @@ public class RDEpubReaderView: UIView {
|
||||
let pageView = preloadController.pageViewForDisplay(
|
||||
pageNum: pageNum,
|
||||
environment: preloadEnvironment,
|
||||
contentViewProvider: pageContentViewForDisplay(pageNum:reusableView:)
|
||||
contentViewProvider: contentViewForPage(_:reusableView:)
|
||||
)
|
||||
// A data-source request is only a candidate. UIPageViewController may
|
||||
// ask for duplicate instances of the current page; activation happens
|
||||
// after the winning controller is actually installed.
|
||||
(pageView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: isReaderPageVisible(pageNum))
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
return pageView
|
||||
}
|
||||
|
||||
@@ -551,10 +554,6 @@ public class RDEpubReaderView: UIView {
|
||||
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 {
|
||||
@@ -564,7 +563,7 @@ public class RDEpubReaderView: UIView {
|
||||
return pageNum == currentPage
|
||||
}
|
||||
|
||||
private func updatePageContentVisibility() {
|
||||
func updatePageContentVisibility() {
|
||||
var visiblePageNumbers = Set<Int>()
|
||||
if currentPage >= 0 {
|
||||
visiblePageNumbers.insert(currentPage)
|
||||
@@ -574,20 +573,57 @@ public class RDEpubReaderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
let pageControllers: [RDEpubReaderPageChildViewController]
|
||||
let visibleCellContents: [(pageNumber: Int, contentView: UIView)]
|
||||
if currentDisplayType == .pageCurl {
|
||||
pageControllers = (pageViewController.viewControllers ?? []).compactMap {
|
||||
$0 as? RDEpubReaderPageChildViewController
|
||||
}
|
||||
visibleCellContents = []
|
||||
} else {
|
||||
pageControllers = []
|
||||
visibleCellContents = collectionView.indexPathsForVisibleItems.compactMap { indexPath in
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) as? RDEpubReaderContentCell,
|
||||
let contentView = cell.containerView else { return nil }
|
||||
return (indexPath.item, contentView)
|
||||
}
|
||||
}
|
||||
|
||||
for indexPath in collectionView.indexPathsForVisibleItems {
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) as? RDEpubReaderContentCell,
|
||||
let contentView = cell.containerView else { continue }
|
||||
for pageController in pageControllers where !visiblePageNumbers.contains(pageController.pageNum) {
|
||||
guard let contentView = pageController.contentView else { continue }
|
||||
(contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: visiblePageNumbers.contains(indexPath.item))
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
}
|
||||
|
||||
for entry in visibleCellContents where !visiblePageNumbers.contains(entry.pageNumber) {
|
||||
(entry.contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
}
|
||||
|
||||
// Suspend every cached non-winner before the attached winner below is
|
||||
// activated. Object identity matters because a page number can have
|
||||
// multiple UIPageViewController candidates.
|
||||
let visibleViewIdentifiers = Set(
|
||||
pageControllers.compactMap { pageController -> ObjectIdentifier? in
|
||||
guard visiblePageNumbers.contains(pageController.pageNum),
|
||||
let contentView = pageController.contentView else { return nil }
|
||||
return ObjectIdentifier(contentView)
|
||||
} + visibleCellContents.compactMap { entry -> ObjectIdentifier? in
|
||||
guard visiblePageNumbers.contains(entry.pageNumber) else { return nil }
|
||||
return ObjectIdentifier(entry.contentView)
|
||||
}
|
||||
)
|
||||
preloadController.updatePageVisibility(visibleViewIdentifiers: visibleViewIdentifiers)
|
||||
|
||||
for pageController in pageControllers where visiblePageNumbers.contains(pageController.pageNum) {
|
||||
guard let contentView = pageController.contentView else { continue }
|
||||
(contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: true)
|
||||
}
|
||||
|
||||
for entry in visibleCellContents where visiblePageNumbers.contains(entry.pageNumber) {
|
||||
(entry.contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,6 +665,11 @@ public class RDEpubReaderView: UIView {
|
||||
}
|
||||
|
||||
private func detachPageViewControllerIfNeeded() {
|
||||
for case let childController as RDEpubReaderPageChildViewController in pageViewController.viewControllers ?? [] {
|
||||
guard let contentView = childController.contentView else { continue }
|
||||
(contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
}
|
||||
if pageViewController.parent != nil {
|
||||
pageViewController.willMove(toParent: nil)
|
||||
}
|
||||
@@ -882,6 +923,17 @@ public class RDEpubReaderView: UIView {
|
||||
}
|
||||
predictedPageDirection = currentPage >= 0 ? safePageNum >= currentPage : nil
|
||||
attachPageViewControllerIfNeeded()
|
||||
|
||||
// Programmatic setViewControllers transitions do not call the
|
||||
// UIPageViewControllerDelegate didFinishAnimating callback. Stop
|
||||
// the outgoing controllers explicitly before replacing them, even
|
||||
// when a duplicate page instance has fallen out of our cache map.
|
||||
for case let childController as RDEpubReaderPageChildViewController in pageViewController.viewControllers ?? [] {
|
||||
guard let contentView = childController.contentView else { continue }
|
||||
(contentView as? RDEpubReaderPageVisibilityHandling)?
|
||||
.readerPageVisibilityDidChange(isVisible: false)
|
||||
}
|
||||
|
||||
if isDualPage {
|
||||
let pair = dualPagePair(for: safePageNum)
|
||||
let leftContent = pageViewForDisplay(pageNum: pair.left)
|
||||
@@ -903,7 +955,11 @@ public class RDEpubReaderView: UIView {
|
||||
self.finishPageCurlTransition()
|
||||
}
|
||||
}
|
||||
let pageChanged = currentPage != pair.left
|
||||
currentPage = pair.left
|
||||
if !pageChanged {
|
||||
updatePageContentVisibility()
|
||||
}
|
||||
primePageCache(around: pair.left, preferredForward: predictedPageDirection)
|
||||
} else {
|
||||
let contentView = pageViewForDisplay(pageNum: safePageNum)
|
||||
@@ -913,7 +969,11 @@ public class RDEpubReaderView: UIView {
|
||||
guard let self else { return }
|
||||
self.finishPageCurlTransition()
|
||||
}
|
||||
let pageChanged = currentPage != safePageNum
|
||||
currentPage = safePageNum
|
||||
if !pageChanged {
|
||||
updatePageContentVisibility()
|
||||
}
|
||||
primePageCache(around: safePageNum, preferredForward: predictedPageDirection)
|
||||
}
|
||||
default:
|
||||
@@ -921,7 +981,11 @@ public class RDEpubReaderView: UIView {
|
||||
collectionView.layoutIfNeeded()
|
||||
predictedPageDirection = currentPage >= 0 ? safePageNum >= currentPage : nil
|
||||
collectionView.setContentOffset(layout.currentContentOffset(count: safePageNum), animated: animated)
|
||||
let pageChanged = currentPage != safePageNum
|
||||
currentPage = safePageNum
|
||||
if !pageChanged {
|
||||
updatePageContentVisibility()
|
||||
}
|
||||
primePageCache(around: safePageNum, preferredForward: predictedPageDirection)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user