- 拆分 ContentDelegates/TextContentView 为独立协调器(InteractionCoordinator、LocationResolution、ExternalLinks、AttachmentTooltip) - 新增 RDEPUBAttachmentTooltipView/OverlayView 附件气泡提示 - 新增 RDEPUBDarkImageAdjuster 暗色模式图片亮度适配 - 新增 RDEPUBSelectionLoupeView 选区放大镜 - 新增 MetadataParseWorker/CancellationController 元数据解析取消机制 - 重构 PresentationRuntime/PaginationCoordinator 精简职责 - 优化 ChapterLoader/WarmupOrchestrator 异步章节加载 - CFI 模块微调与 NoteModels 更新 - 清理冗余文档,更新架构/UML/业务逻辑文档 Co-Authored-By: Claude <noreply@anthropic.com>
818 lines
29 KiB
Swift
818 lines
29 KiB
Swift
|
|
import UIKit
|
|
|
|
public class RDReaderView: UIView {
|
|
|
|
enum TapEvent {
|
|
|
|
case none
|
|
|
|
case left
|
|
|
|
case center
|
|
|
|
case right
|
|
}
|
|
|
|
lazy var pageViewController: UIPageViewController = {
|
|
let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil)
|
|
pageVC.delegate = self
|
|
pageVC.dataSource = self
|
|
return pageVC
|
|
}()
|
|
|
|
lazy var layout: RDReaderFlowLayout = {
|
|
let layout = RDReaderFlowLayout(displayType: .horizontalScroll)
|
|
layout.dataSource = self
|
|
layout.delegate = self
|
|
return layout
|
|
}()
|
|
|
|
lazy var collectionView: UICollectionView = {
|
|
|
|
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
collectionView.backgroundColor = UIColor.clear
|
|
collectionView.accessibilityIdentifier = "epub.reader.paging"
|
|
return collectionView
|
|
}()
|
|
|
|
private let spreadResolver = RDReaderSpreadResolver()
|
|
|
|
private let tapRegionHandler = RDReaderTapRegionHandler()
|
|
|
|
let preloadController = RDReaderPreloadController()
|
|
|
|
var pagingController = RDReaderPagingController()
|
|
|
|
public var currentPage: Int = -1 {
|
|
didSet {
|
|
if let delegate = delegate, currentPage != oldValue, delegate.responds(to: #selector(RDReaderDelegate.pageNum(readerView:pageNum:))) {
|
|
delegate.pageNum(readerView: self, pageNum: currentPage)
|
|
}
|
|
}
|
|
}
|
|
|
|
private(set) lazy var tapGestureRecognizer: UITapGestureRecognizer = {
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(tap:)))
|
|
tap.delegate = self
|
|
return tap
|
|
}()
|
|
|
|
private var tapEvent: TapEvent = .none {
|
|
didSet {
|
|
let isRTL = pageDirection == .rightToLeft
|
|
switch tapEvent {
|
|
case .left:
|
|
if currentDisplayType != .pageCurl {
|
|
if isRTL { goNextPage() } else { goPreviousPage() }
|
|
}
|
|
case .right:
|
|
if currentDisplayType != .pageCurl {
|
|
if isRTL { goPreviousPage() } else { goNextPage() }
|
|
}
|
|
case .center:
|
|
tapCenter()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
private func goNextPage() {
|
|
let totalPages = numberOfPages()
|
|
if let target = spreadResolver.nextPage(
|
|
from: currentPage,
|
|
totalPages: totalPages,
|
|
pagesPerScreen: pagesPerScreen,
|
|
coverPageIndex: coverPageIndex,
|
|
forward: true
|
|
) {
|
|
transitionToPage(pageNum: target, animated: true)
|
|
}
|
|
}
|
|
|
|
private func goPreviousPage() {
|
|
let totalPages = numberOfPages()
|
|
if let target = spreadResolver.nextPage(
|
|
from: currentPage,
|
|
totalPages: totalPages,
|
|
pagesPerScreen: pagesPerScreen,
|
|
coverPageIndex: coverPageIndex,
|
|
forward: false
|
|
) {
|
|
transitionToPage(pageNum: target, animated: true)
|
|
}
|
|
}
|
|
|
|
private let legacyDataSourceAdapter = RDReaderLegacyDataSourceAdapter()
|
|
|
|
public weak var dataSource: RDReaderDataSource? {
|
|
didSet {
|
|
legacyDataSourceAdapter.dataSource = dataSource
|
|
}
|
|
}
|
|
|
|
public weak var pageProvider: RDReaderPageProvider?
|
|
|
|
public weak var delegate: RDReaderDelegate? = nil
|
|
|
|
public var currentDisplayType: RDReaderView.DisplayType = .pageCurl
|
|
|
|
public var toolViewAnimationDuration: TimeInterval = 0.3
|
|
|
|
var onToolViewVisibilityChanged: ((Bool) -> Void)?
|
|
|
|
var searchBarView: UIView?
|
|
|
|
public var landscapeDualPageEnabled: Bool = false
|
|
|
|
public var pageDirection: RDReaderView.PageDirection = .leftToRight
|
|
|
|
public var coverPageIndex: Int? = nil
|
|
|
|
private var resolvedPageProvider: RDReaderPageProvider? {
|
|
pageProvider ?? legacyDataSourceAdapter
|
|
}
|
|
|
|
func numberOfPages() -> Int {
|
|
resolvedPageProvider?.numberOfPages(in: self) ?? 0
|
|
}
|
|
|
|
func pageReuseIdentifier(for pageNum: Int) -> String? {
|
|
resolvedPageProvider?.pageIdentifier?(in: self, index: pageNum)
|
|
}
|
|
|
|
func contentViewForPage(_ pageNum: Int, reusableView: UIView?) -> UIView {
|
|
resolvedPageProvider?.readerView(self, viewForPageAt: pageNum, reusableView: reusableView) ?? UIView()
|
|
}
|
|
|
|
private func resolvedTopChromeView() -> UIView? {
|
|
resolvedPageProvider?.readerViewTopChrome?(self)
|
|
}
|
|
|
|
private func resolvedBottomChromeView() -> UIView? {
|
|
resolvedPageProvider?.readerViewBottomChrome?(self)
|
|
}
|
|
|
|
func refreshToolViewsFromProviderIfNeeded() {
|
|
if topToolView == nil {
|
|
topToolView = resolvedTopChromeView()
|
|
}
|
|
if bottomToolView == nil {
|
|
bottomToolView = resolvedBottomChromeView()
|
|
}
|
|
}
|
|
|
|
private var hasCoverPage: Bool {
|
|
return coverPageIndex != nil
|
|
}
|
|
|
|
var isLandscape: Bool {
|
|
return bounds.width > bounds.height
|
|
}
|
|
|
|
public var pagesPerScreen: Int {
|
|
if !landscapeDualPageEnabled { return 1 }
|
|
if currentDisplayType == .verticalScroll { return 1 }
|
|
return isLandscape ? 2 : 1
|
|
}
|
|
|
|
public func isFullScreenPage(_ pageNum: Int) -> Bool {
|
|
spreadResolver.isFullScreenPage(
|
|
pageNum,
|
|
landscapeDualPageEnabled: landscapeDualPageEnabled,
|
|
isLandscape: isLandscape,
|
|
coverPageIndex: coverPageIndex
|
|
)
|
|
}
|
|
|
|
private func dualPagePair(for pageNum: Int) -> (left: Int, right: Int?) {
|
|
let totalPages = numberOfPages()
|
|
return spreadResolver.dualPagePair(
|
|
for: pageNum,
|
|
totalPages: totalPages,
|
|
coverPageIndex: coverPageIndex
|
|
)
|
|
}
|
|
|
|
private func adjacentDualPage(from pageNum: Int, forward: Bool) -> Int? {
|
|
let totalPages = numberOfPages()
|
|
return spreadResolver.adjacentDualPage(
|
|
from: pageNum,
|
|
totalPages: totalPages,
|
|
coverPageIndex: coverPageIndex,
|
|
forward: forward
|
|
)
|
|
}
|
|
|
|
private func clampedPageNumber(_ pageNum: Int) -> Int? {
|
|
let totalPages = numberOfPages()
|
|
guard totalPages > 0 else { return nil }
|
|
return min(max(pageNum, 0), totalPages - 1)
|
|
}
|
|
|
|
private var previousIsLandscape: Bool?
|
|
|
|
var contentViews = [String : UIView.Type]()
|
|
|
|
var willPreviousTransitionToViewController: UIViewController? = nil
|
|
|
|
var willNextTransitionToViewController: UIViewController? = nil
|
|
|
|
var willTransitionToViewController: UIViewController? = nil
|
|
|
|
var topToolView: UIView?
|
|
|
|
var bottomToolView: UIView?
|
|
|
|
var topToolViewHeightConstraint: NSLayoutConstraint?
|
|
|
|
var bottomToolViewHeightConstraint: NSLayoutConstraint?
|
|
|
|
var isShowToolView: Bool = false
|
|
|
|
private var isTransitioning: Bool {
|
|
get { pagingController.isTransitioning }
|
|
set { pagingController.isTransitioning = newValue }
|
|
}
|
|
|
|
private var didBuildUI: Bool {
|
|
get { pagingController.didBuildUI }
|
|
set { pagingController.didBuildUI = newValue }
|
|
}
|
|
|
|
var predictedPageDirection: Bool?
|
|
|
|
public var preloadRadius: Int {
|
|
get { preloadController.radius }
|
|
set { preloadController.radius = newValue }
|
|
}
|
|
|
|
static let blankPageNum = Int.max
|
|
|
|
static let blankEndPageNum = Int.max - 1
|
|
|
|
private typealias PageTransitionRequest = RDReaderPagingController.PageTransitionRequest
|
|
|
|
private let registeredSelectionLongPressRecognizers = NSHashTable<UILongPressGestureRecognizer>.weakObjects()
|
|
|
|
private let selectionTapSuppressedContentViews = NSHashTable<UIView>.weakObjects()
|
|
|
|
private let selectionPagingSuppressedContentViews = NSHashTable<UIView>.weakObjects()
|
|
|
|
private var isPagingInteractionSuppressed = false
|
|
|
|
private var pendingTransitionRequest: PageTransitionRequest? {
|
|
get { pagingController.pendingTransitionRequest }
|
|
set { pagingController.pendingTransitionRequest = newValue }
|
|
}
|
|
|
|
var isPageCurlTransitioning: Bool {
|
|
currentDisplayType == .pageCurl && pagingController.isTransitioning
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
accessibilityIdentifier = "epub.reader.content"
|
|
isAccessibilityElement = false
|
|
}
|
|
|
|
public override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
preloadController.setHostFrame(bounds)
|
|
updateToolViewHeightConstraintsIfNeeded()
|
|
let nowLandscape = isLandscape
|
|
if let prev = previousIsLandscape, prev != nowLandscape {
|
|
previousIsLandscape = nowLandscape
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.orientationChanged(isNowLandscape: nowLandscape)
|
|
}
|
|
} else if previousIsLandscape == nil {
|
|
previousIsLandscape = nowLandscape
|
|
layout.isLandscapeDualPage = landscapeDualPageEnabled && nowLandscape
|
|
}
|
|
}
|
|
|
|
private func orientationChanged(isNowLandscape: Bool) {
|
|
let savedPage = max(0, currentPage)
|
|
|
|
delegate?.readerViewOrientationWillChange?(readerView: self, isLandscape: isNowLandscape)
|
|
|
|
layout.isLandscapeDualPage = landscapeDualPageEnabled && isNowLandscape
|
|
layout.coverPageIndex = coverPageIndex
|
|
|
|
switch currentDisplayType {
|
|
case .pageCurl:
|
|
|
|
rebuildPageViewController()
|
|
transitionToPage(pageNum: savedPage)
|
|
primePageCache(around: savedPage, preferredForward: predictedPageDirection)
|
|
default:
|
|
|
|
UIView.performWithoutAnimation {
|
|
|
|
collectionView.collectionViewLayout.invalidateLayout()
|
|
collectionView.reloadData()
|
|
collectionView.layoutIfNeeded()
|
|
|
|
let totalPages = numberOfPages()
|
|
let safePage = min(savedPage, max(0, totalPages - 1))
|
|
let targetOffset = layout.currentContentOffset(count: safePage)
|
|
collectionView.setContentOffset(targetOffset, animated: false)
|
|
primePageCache(around: safePage, preferredForward: predictedPageDirection)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func createPageViewController(isDualPage: Bool) -> UIPageViewController {
|
|
let options: [UIPageViewController.OptionsKey: Any]?
|
|
if isDualPage {
|
|
options = [.spineLocation: NSNumber(value: UIPageViewController.SpineLocation.mid.rawValue)]
|
|
} else {
|
|
options = nil
|
|
}
|
|
let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: options)
|
|
pageVC.delegate = self
|
|
pageVC.dataSource = self
|
|
pageVC.isDoubleSided = isDualPage
|
|
return pageVC
|
|
}
|
|
|
|
private func rebuildPageViewController() {
|
|
detachPageViewControllerIfNeeded()
|
|
|
|
let isDualPage = landscapeDualPageEnabled && isLandscape
|
|
let pageVC = createPageViewController(isDualPage: isDualPage)
|
|
pageViewController = pageVC
|
|
attachPageViewControllerIfNeeded()
|
|
}
|
|
|
|
private func detectPageViewControllerFault(_ pageVC: UIPageViewController) -> Bool {
|
|
guard currentDisplayType == .pageCurl else { return false }
|
|
let expectedCount = (landscapeDualPageEnabled && isLandscape) ? 2 : 1
|
|
guard let viewControllers = pageVC.viewControllers,
|
|
viewControllers.count == expectedCount else {
|
|
return true
|
|
}
|
|
|
|
let childViewControllers = viewControllers.compactMap { $0 as? RDReaderPageChildViewController }
|
|
guard childViewControllers.count == expectedCount else {
|
|
return true
|
|
}
|
|
|
|
if expectedCount == 1 {
|
|
return childViewControllers.first?.pageNum != currentPage
|
|
}
|
|
|
|
let expectedPair = dualPagePair(for: currentPage)
|
|
let expectedRightPage = expectedPair.right
|
|
?? (isFullScreenPage(expectedPair.left) ? RDReaderView.blankPageNum : RDReaderView.blankEndPageNum)
|
|
return childViewControllers[0].pageNum != expectedPair.left
|
|
|| childViewControllers[1].pageNum != expectedRightPage
|
|
}
|
|
|
|
private func patchPageViewControllerFault() {
|
|
guard currentPage >= 0 else { return }
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self else { return }
|
|
self.pagingController.resetPendingState()
|
|
self.willPreviousTransitionToViewController = nil
|
|
self.willNextTransitionToViewController = nil
|
|
self.willTransitionToViewController = nil
|
|
self.invalidatePageCaches()
|
|
self.rebuildPageViewController()
|
|
self.transitionToPage(pageNum: self.currentPage, animated: false)
|
|
}
|
|
}
|
|
|
|
private func shouldQueuePageTransition(_ request: PageTransitionRequest) -> Bool {
|
|
pagingController.shouldQueuePageTransition(request, currentDisplayType: currentDisplayType)
|
|
}
|
|
|
|
func finishPageCurlTransition(repairFaultIfNeeded: Bool = true) {
|
|
willPreviousTransitionToViewController = nil
|
|
willNextTransitionToViewController = nil
|
|
willTransitionToViewController = nil
|
|
|
|
if repairFaultIfNeeded, detectPageViewControllerFault(pageViewController) {
|
|
patchPageViewControllerFault()
|
|
return
|
|
}
|
|
|
|
if let pending = pagingController.finishPageCurlTransition() {
|
|
transitionToPage(pageNum: pending.pageNum, animated: pending.animated)
|
|
}
|
|
|
|
updatePagingInteractionSuppression()
|
|
}
|
|
|
|
private var preloadEnvironment: RDReaderPreloadController.Environment {
|
|
RDReaderPreloadController.Environment(
|
|
displayType: currentDisplayType,
|
|
isLandscape: isLandscape,
|
|
pagesPerScreen: pagesPerScreen,
|
|
boundsSize: bounds.size,
|
|
landscapeDualPageEnabled: landscapeDualPageEnabled,
|
|
coverPageIndex: coverPageIndex,
|
|
totalPages: numberOfPages(),
|
|
spreadResolver: spreadResolver
|
|
)
|
|
}
|
|
|
|
private func invalidatePageCaches() {
|
|
preloadController.invalidate(environment: preloadEnvironment)
|
|
}
|
|
|
|
func pageViewForDisplay(pageNum: Int) -> UIView {
|
|
preloadController.pageViewForDisplay(
|
|
pageNum: pageNum,
|
|
environment: preloadEnvironment,
|
|
contentViewProvider: pageContentViewForPreload(pageNum:reusableView:)
|
|
)
|
|
}
|
|
|
|
func primePageCache(around pageNum: Int, preferredForward: Bool? = nil) {
|
|
preloadController.prime(
|
|
around: pageNum,
|
|
preferredForward: preferredForward,
|
|
parentView: self,
|
|
environment: preloadEnvironment,
|
|
contentViewProvider: pageContentViewForPreload(pageNum:reusableView:)
|
|
)
|
|
}
|
|
|
|
private func pageContentViewForPreload(pageNum: Int, reusableView: UIView?) -> UIView? {
|
|
contentViewForPage(pageNum, reusableView: reusableView)
|
|
}
|
|
|
|
public override func didMoveToSuperview() {
|
|
super.didMoveToSuperview()
|
|
|
|
if superview != nil {
|
|
makeUI()
|
|
}
|
|
}
|
|
|
|
private func attachPageViewControllerIfNeeded() {
|
|
guard let parentViewController = self.ss_superViewController else { return }
|
|
|
|
var didAddToParent = false
|
|
if pageViewController.parent !== parentViewController {
|
|
if pageViewController.parent != nil {
|
|
pageViewController.willMove(toParent: nil)
|
|
pageViewController.view.removeFromSuperview()
|
|
pageViewController.removeFromParent()
|
|
}
|
|
parentViewController.addChild(pageViewController)
|
|
didAddToParent = true
|
|
}
|
|
|
|
pageViewController.view.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
|
|
pageViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
if pageViewController.view.superview !== self {
|
|
insertSubview(pageViewController.view, at: 0)
|
|
}
|
|
|
|
if didAddToParent {
|
|
pageViewController.didMove(toParent: parentViewController)
|
|
}
|
|
|
|
if currentDisplayType == .pageCurl, !isTransitioning, pendingTransitionRequest != nil {
|
|
finishPageCurlTransition(repairFaultIfNeeded: false)
|
|
}
|
|
}
|
|
|
|
private func detachPageViewControllerIfNeeded() {
|
|
if pageViewController.parent != nil {
|
|
pageViewController.willMove(toParent: nil)
|
|
}
|
|
pageViewController.view.removeFromSuperview()
|
|
if pageViewController.parent != nil {
|
|
pageViewController.removeFromParent()
|
|
}
|
|
}
|
|
|
|
private func makeUI() {
|
|
guard !didBuildUI else {
|
|
if currentDisplayType == .pageCurl {
|
|
attachPageViewControllerIfNeeded()
|
|
}
|
|
return
|
|
}
|
|
didBuildUI = true
|
|
|
|
collectionView.dataSource = self
|
|
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(UICollectionViewCell.self))
|
|
collectionView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
|
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
|
if currentDisplayType == .pageCurl {
|
|
attachPageViewControllerIfNeeded()
|
|
}
|
|
preloadController.ensureHostView(in: self)
|
|
preloadController.initializeSignature(preloadEnvironment)
|
|
|
|
addGestureRecognizer(tapGestureRecognizer)
|
|
|
|
tapGestureRecognizer.cancelsTouchesInView = false
|
|
|
|
}
|
|
|
|
@objc private func tapAction(tap: UITapGestureRecognizer) {
|
|
let point = tap.location(in: tap.view)
|
|
let hitView = hitTest(point, with: nil)
|
|
if containsTextContentView(in: hitView) {
|
|
return
|
|
}
|
|
if shouldSuppressChromeToggle(for: hitView, point: point) {
|
|
return
|
|
}
|
|
handleResolvedTap(at: point, hitView: hitView, in: tap.view)
|
|
}
|
|
|
|
private func shouldSuppressChromeToggle(for hitView: UIView?, point: CGPoint) -> Bool {
|
|
if selectionTapSuppressedContentViews.allObjects.isEmpty == false {
|
|
return true
|
|
}
|
|
var currentView = hitView
|
|
while let view = currentView {
|
|
if let textContentView = view as? RDEPUBTextContentView {
|
|
let localPoint = convert(point, to: textContentView)
|
|
return textContentView.shouldSuppressReaderTap(at: localPoint)
|
|
}
|
|
currentView = view.superview
|
|
}
|
|
return false
|
|
}
|
|
|
|
func registerSelectionGestureDependenciesIfNeeded(for contentView: UIView) {
|
|
guard let textContentView = contentView as? RDEPUBTextContentView else { return }
|
|
let longPressGesture = textContentView.selectionLongPressGestureRecognizer
|
|
if registeredSelectionLongPressRecognizers.allObjects.contains(where: { $0 === longPressGesture }) {
|
|
return
|
|
}
|
|
tapGestureRecognizer.require(toFail: longPressGesture)
|
|
registeredSelectionLongPressRecognizers.add(longPressGesture)
|
|
}
|
|
|
|
func updateSelectionTapSuppression(for contentView: UIView, isSuppressed: Bool) {
|
|
if isSuppressed {
|
|
if selectionTapSuppressedContentViews.allObjects.contains(where: { $0 === contentView }) == false {
|
|
selectionTapSuppressedContentViews.add(contentView)
|
|
}
|
|
} else {
|
|
selectionTapSuppressedContentViews.remove(contentView)
|
|
}
|
|
}
|
|
|
|
func updateSelectionPagingSuppression(for contentView: UIView, isSuppressed: Bool) {
|
|
if isSuppressed {
|
|
if selectionPagingSuppressedContentViews.allObjects.contains(where: { $0 === contentView }) == false {
|
|
selectionPagingSuppressedContentViews.add(contentView)
|
|
}
|
|
} else {
|
|
selectionPagingSuppressedContentViews.remove(contentView)
|
|
}
|
|
updatePagingInteractionSuppression()
|
|
}
|
|
|
|
func handleContentTap(at point: CGPoint, in sourceView: UIView) {
|
|
let localPoint = sourceView.convert(point, to: self)
|
|
handleResolvedTap(at: localPoint, hitView: nil, in: self)
|
|
}
|
|
|
|
private func handleResolvedTap(at point: CGPoint, hitView: UIView?, in tapView: UIView?) {
|
|
if isShowToolView {
|
|
if let top = topToolView, isHitView(hitView, inside: top, point: point) { return }
|
|
if let bottom = bottomToolView, isHitView(hitView, inside: bottom, point: point) { return }
|
|
}
|
|
guard let viewFrame = tapView?.frame else { return }
|
|
tapEvent = tapRegionHandler.resolveTapEvent(
|
|
point: point,
|
|
viewFrame: viewFrame,
|
|
isToolViewVisible: isShowToolView
|
|
)
|
|
}
|
|
|
|
private func containsTextContentView(in hitView: UIView?) -> Bool {
|
|
var currentView = hitView
|
|
while let view = currentView {
|
|
if view is RDEPUBTextContentView {
|
|
return true
|
|
}
|
|
currentView = view.superview
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func updatePagingInteractionSuppression() {
|
|
let shouldSuppress = activePagingSuppressionContentViews().isEmpty == false
|
|
guard shouldSuppress != isPagingInteractionSuppressed else { return }
|
|
isPagingInteractionSuppressed = shouldSuppress
|
|
setPagingInteractionEnabled(!shouldSuppress)
|
|
}
|
|
|
|
private func activePagingSuppressionContentViews() -> [UIView] {
|
|
selectionPagingSuppressedContentViews.allObjects.filter { contentView in
|
|
guard contentView.window != nil,
|
|
contentView.isDescendant(of: self),
|
|
contentView.bounds.isEmpty == false else {
|
|
return false
|
|
}
|
|
let frameInReader = contentView.convert(contentView.bounds, to: self)
|
|
return frameInReader.intersects(bounds)
|
|
}
|
|
}
|
|
|
|
private func setPagingInteractionEnabled(_ isEnabled: Bool) {
|
|
collectionView.isScrollEnabled = isEnabled
|
|
pageViewController.gestureRecognizers.forEach { $0.isEnabled = isEnabled }
|
|
}
|
|
|
|
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType) {
|
|
let previousDisplayType = currentDisplayType
|
|
self.currentDisplayType = displayType
|
|
if currentPage == -1 {
|
|
currentPage = 0
|
|
}
|
|
if previousDisplayType != displayType {
|
|
invalidatePageCaches()
|
|
}
|
|
|
|
layout.isLandscapeDualPage = landscapeDualPageEnabled && isLandscape
|
|
layout.coverPageIndex = coverPageIndex
|
|
switch displayType {
|
|
case .pageCurl:
|
|
self.collectionView.removeFromSuperview()
|
|
self.collectionView.transform = .identity
|
|
attachPageViewControllerIfNeeded()
|
|
rebuildPageViewController()
|
|
transitionToPage(pageNum: currentPage)
|
|
primePageCache(around: currentPage, preferredForward: predictedPageDirection)
|
|
default:
|
|
detachPageViewControllerIfNeeded()
|
|
|
|
if pageDirection == .rightToLeft && displayType != .verticalScroll {
|
|
collectionView.transform = CGAffineTransform(scaleX: -1, y: 1)
|
|
} else {
|
|
collectionView.transform = .identity
|
|
}
|
|
|
|
collectionView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
|
|
insertSubview(self.collectionView, at: 0)
|
|
layout.displayType = displayType
|
|
transitionToPage(pageNum: currentPage)
|
|
primePageCache(around: currentPage, preferredForward: predictedPageDirection)
|
|
}
|
|
}
|
|
|
|
public func transitionToPage(pageNum: Int, animated: Bool = false) {
|
|
guard let safePageNum = clampedPageNumber(pageNum) else { return }
|
|
switch currentDisplayType {
|
|
case .pageCurl:
|
|
let request = PageTransitionRequest(pageNum: safePageNum, animated: animated)
|
|
if shouldQueuePageTransition(request) {
|
|
return
|
|
}
|
|
|
|
let isDualPage = landscapeDualPageEnabled && isLandscape
|
|
|
|
let direction: UIPageViewController.NavigationDirection
|
|
if pageDirection == .rightToLeft {
|
|
direction = safePageNum > currentPage ? .reverse : .forward
|
|
} else {
|
|
direction = safePageNum > currentPage ? .forward : .reverse
|
|
}
|
|
predictedPageDirection = currentPage >= 0 ? safePageNum >= currentPage : nil
|
|
attachPageViewControllerIfNeeded()
|
|
if isDualPage {
|
|
let pair = dualPagePair(for: safePageNum)
|
|
let leftContent = pageViewForDisplay(pageNum: pair.left)
|
|
let leftVC = RDReaderPageChildViewController(contentView: leftContent, pageNum: pair.left)
|
|
isTransitioning = animated
|
|
if let rightPage = pair.right {
|
|
let rightContent = pageViewForDisplay(pageNum: rightPage)
|
|
let rightVC = RDReaderPageChildViewController(contentView: rightContent, pageNum: rightPage)
|
|
pageViewController.setViewControllers([leftVC, rightVC], direction: animated ? direction : .forward, animated: animated) { [weak self] _ in
|
|
guard let self else { return }
|
|
self.finishPageCurlTransition()
|
|
}
|
|
} else {
|
|
|
|
let blankNum = isFullScreenPage(pair.left) ? RDReaderView.blankPageNum : RDReaderView.blankEndPageNum
|
|
let emptyVC = RDReaderPageChildViewController(contentView: UIView(), pageNum: blankNum)
|
|
pageViewController.setViewControllers([leftVC, emptyVC], direction: animated ? direction : .forward, animated: animated) { [weak self] _ in
|
|
guard let self else { return }
|
|
self.finishPageCurlTransition()
|
|
}
|
|
}
|
|
currentPage = pair.left
|
|
primePageCache(around: pair.left, preferredForward: predictedPageDirection)
|
|
} else {
|
|
let contentView = pageViewForDisplay(pageNum: safePageNum)
|
|
let vc = RDReaderPageChildViewController(contentView: contentView, pageNum: safePageNum)
|
|
isTransitioning = animated
|
|
pageViewController.setViewControllers([vc], direction: animated ? direction : .forward, animated: animated) { [weak self] _ in
|
|
guard let self else { return }
|
|
self.finishPageCurlTransition()
|
|
}
|
|
currentPage = safePageNum
|
|
primePageCache(around: safePageNum, preferredForward: predictedPageDirection)
|
|
}
|
|
default:
|
|
collectionView.reloadData()
|
|
collectionView.layoutIfNeeded()
|
|
predictedPageDirection = currentPage >= 0 ? safePageNum >= currentPage : nil
|
|
collectionView.setContentOffset(layout.currentContentOffset(count: safePageNum), animated: animated)
|
|
currentPage = safePageNum
|
|
primePageCache(around: safePageNum, preferredForward: predictedPageDirection)
|
|
}
|
|
}
|
|
|
|
public func reloadData() {
|
|
switchReaderDisplayType(currentDisplayType)
|
|
topToolView = resolvedTopChromeView()
|
|
bottomToolView = resolvedBottomChromeView()
|
|
}
|
|
|
|
public func reloadPageCountOnly() {
|
|
if currentDisplayType == .pageCurl {
|
|
if !isPageCurlTransitioning, currentPage >= 0, numberOfPages() > 0 {
|
|
transitionToPage(pageNum: currentPage, animated: false)
|
|
}
|
|
} else {
|
|
collectionView.reloadData()
|
|
}
|
|
topToolView = resolvedTopChromeView()
|
|
bottomToolView = resolvedBottomChromeView()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
}
|
|
|
|
extension RDReaderView: RDReaderPageNavigating {
|
|
public func reloadPages() {
|
|
reloadData()
|
|
}
|
|
|
|
public func transition(to page: Int, animated: Bool) {
|
|
transitionToPage(pageNum: page, animated: animated)
|
|
}
|
|
}
|
|
|
|
extension RDReaderView: UIGestureRecognizerDelegate {
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
guard gestureRecognizer === tapGestureRecognizer else { return true }
|
|
|
|
if selectionTapSuppressedContentViews.allObjects.isEmpty == false {
|
|
return false
|
|
}
|
|
|
|
let point = touch.location(in: self)
|
|
if containsTextContentView(in: touch.view) {
|
|
return false
|
|
}
|
|
if let topToolView, isHitView(touch.view, inside: topToolView, point: point) {
|
|
return false
|
|
}
|
|
if let bottomToolView, isHitView(touch.view, inside: bottomToolView, point: point) {
|
|
return false
|
|
}
|
|
if let searchBarView, isHitView(touch.view, inside: searchBarView, point: point) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public func gestureRecognizer(
|
|
_ gestureRecognizer: UIGestureRecognizer,
|
|
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
|
|
) -> Bool {
|
|
gestureRecognizer === tapGestureRecognizer
|
|
}
|
|
}
|
|
|
|
private var cellViewKey: Int8 = 0
|
|
|
|
extension UIView {
|
|
|
|
var ss_superViewController: UIViewController? {
|
|
var next = self.next
|
|
while next != nil {
|
|
if next is UIViewController {
|
|
return next as? UIViewController
|
|
} else {
|
|
next = next!.next
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
}
|