import UIKit /// 页面创建、缩放状态和点击分区。与 EPUB 的 `RDEpubReaderView+ContentAccess` /// 及 Paging 模块保持同一职责边界。 extension RDPDFReaderView { func makePageView(for page: Int) -> UIView { if usesLandscapeSpread { let left = dataSource?.pageContentView(readerView: self, pageNum: page) ?? UIView() left.tag = page let next = page + 1 let right: UIView? if next < pageCount() { let view = dataSource?.pageContentView(readerView: self, pageNum: next) ?? UIView() view.tag = next right = view } else { right = nil } let spread = RDPDFReaderPageSpreadView(leftPage: left, rightPage: right) spread.tag = page configureInteraction(for: spread) return spread } let view = dataSource?.pageContentView(readerView: self, pageNum: page) ?? UIView() view.tag = page configureInteraction(for: view) return view } func configureInteraction(for view: UIView) { guard let page = view as? RDPDFReaderPageInteractable else { return } // 所有阅读模式统一由 ReaderView 决定何时进入独立放大层;页面本身不再 // 各自接管 pinch/double-tap,避免横屏左右页出现两套缩放坐标。 page.readerSetInternalGesturesEnabled(false) page.readerContentTapHandler = { [weak self, weak view] point in guard let self, let view else { return } self.handleContentTap(view.convert(point, to: self)) } page.readerZoomStateChangedHandler = { [weak self, weak view] zoomed in guard let self, view?.tag == self.currentPage else { return } self.isCurrentPageZoomed = zoomed self.updatePagingState() } page.readerSelectionStateChangedHandler = { [weak self, weak view] active in guard let self, view?.tag == self.currentPage else { return } self.isCurrentPageTextSelectionActive = active self.updatePagingState() } } func synchronizePageState() { isCurrentPageZoomed = (pageContentView(pageNum: currentPage) as? RDPDFReaderPageInteractable)?.readerIsZoomed ?? false isCurrentPageTextSelectionActive = (pageContentView(pageNum: currentPage) as? RDPDFReaderPageInteractable)?.readerHasActiveTextSelection ?? false updatePagingState() } func updatePagingState() { let enabled = isPagingEnabled && !isCurrentPageZoomed && !isCurrentPageTextSelectionActive collectionView.isScrollEnabled = enabled collectionView.panGestureRecognizer.isEnabled = enabled // 仿真翻页由 UIPageViewController 自己的手势驱动,不能只关闭外层 // collectionView。画笔模式调用 `setPagingEnabled(false)` 后一并禁用它, // 这样单指始终交给内容页里的 DrawingCanvas,不会卷页。 nativeCurlPageController?.gestureRecognizers.forEach { $0.isEnabled = enabled } curlPanGestureRecognizer.isEnabled = nativeCurlPageController == nil && currentDisplayType == .pageCurl && isPagingEnabled && !isCurrentPageTextSelectionActive } func transitionCurl(to page: Int, animated: Bool) { guard let controller = nativeCurlPageController else { return } let left = RDPDFReaderPageChildViewController(contentView: makeIndividualPageView(for: page), page: page) var children = [left] if usesLandscapeSpread { let rightPage = page + 1 let rightContent = rightPage < pageCount() ? makeIndividualPageView(for: rightPage) : UIView() children.append(RDPDFReaderPageChildViewController(contentView: rightContent, page: rightPage)) } let direction: UIPageViewController.NavigationDirection = page >= currentPage ? .forward : .reverse controller.setViewControllers(children, direction: direction, animated: animated) { [weak self] _ in self?.updateNativeCurlBookFrame() } currentPage = page if !animated { updateNativeCurlBookFrame() } } func makeIndividualPageView(for page: Int) -> UIView { let view = dataSource?.pageContentView(readerView: self, pageNum: page) ?? UIView() view.tag = page configureSpreadAlignment(for: view, page: page) configureInteraction(for: view) return view } func configureSpreadAlignment(for view: UIView, page: Int) { if let pdfPage = view as? RDPDFReaderPageView { if usesLandscapeSpread { // 原生双页卷页以偶数页为左页、奇数页为右页。 // 左页内容贴右边,右页内容贴左边,两张 PDF 的纸张边缘直接相接。 pdfPage.setSpreadContentAlignment(page.isMultiple(of: 2) ? .trailing : .leading) } else { pdfPage.setSpreadContentAlignment(.centered) } } } @objc func handleCurlPan(_ gesture: UIPanGestureRecognizer) { guard let page = curlContentView as? RDPDFReaderPageInteractable else { return } switch gesture.state { case .began: guard !page.readerHasActiveTextSelection else { curlPanInteraction = .idle return } if page.readerIsZoomed { curlPanInteraction = .contentPan page.readerBeginExternalPan() } else { curlPanInteraction = .pageTurn(startPage: currentPage) } case .changed: guard !page.readerHasActiveTextSelection else { curlPanInteraction = .idle return } if case .contentPan = curlPanInteraction { page.readerUpdateExternalPan(translation: gesture.translation(in: curlHostView)) } case .ended: guard !page.readerHasActiveTextSelection else { curlPanInteraction = .idle return } finishCurlPan(gesture, page: page, cancelled: false) case .cancelled, .failed: finishCurlPan(gesture, page: page, cancelled: true) default: break } } private func finishCurlPan( _ gesture: UIPanGestureRecognizer, page: RDPDFReaderPageInteractable, cancelled: Bool ) { let interaction = curlPanInteraction curlPanInteraction = .idle switch interaction { case .contentPan: page.readerEndExternalPan() case let .pageTurn(startPage): guard !cancelled, currentPage == startPage else { return } let translation = gesture.translation(in: curlHostView) let velocity = gesture.velocity(in: curlHostView) guard abs(translation.x) > abs(translation.y) else { return } let crossedDistance = abs(translation.x) >= max(44, curlHostView.bounds.width * 0.12) let crossedVelocity = abs(velocity.x) >= 450 guard crossedDistance || crossedVelocity else { return } let direction = abs(translation.x) >= 4 ? translation.x : velocity.x let step = usesLandscapeSpread ? 2 : 1 transitionToPage(pageNum: startPage + (direction < 0 ? step : -step), animated: true) case .idle: break case .pinching: // 捏合拥有当前触摸序列,pan 的结束事件不能改变它的状态。 curlPanInteraction = .pinching } } @objc func handleCurlPinch(_ gesture: UIPinchGestureRecognizer) { guard currentDisplayType == .pageCurl, let contentView = curlContentView, let page = contentView as? RDPDFReaderPageInteractable else { return } let point = gesture.location(in: contentView) switch gesture.state { case .began: if case .contentPan = curlPanInteraction { page.readerEndExternalPan() } // 第二根手指到来后,捏合优先,已产生的单指翻页候选立即失效。 curlPanInteraction = .pinching page.readerBeginExternalPinch(at: point) case .changed: if case .pinching = curlPanInteraction { page.readerUpdateExternalPinch(scale: gesture.scale, at: point) } case .ended, .cancelled, .failed: page.readerEndExternalPinch() // 重置 recognizer,确保捏合结束后仍留在屏幕上的单根手指不会在 // 同一触摸序列中重新变成翻页或内容拖动。 curlPanGestureRecognizer.isEnabled = false curlPanInteraction = .idle curlPanGestureRecognizer.isEnabled = currentDisplayType == .pageCurl && isPagingEnabled default: break } } func handleContentTap(_ point: CGPoint) { if isCurrentPageTextSelectionActive { (curlContentView as? RDPDFReaderPageInteractable)?.readerClearTextSelection() return } // EPUB 的仿真模式由 UIPageViewController 接管翻页;内容点击只负责显隐工具栏。 // PDF 的自定义仿真容器保持相同语义,避免边缘点击绕开横向翻页手势。 guard currentDisplayType != .pageCurl else { toggleToolbars() return } // 手机横屏单页是连续竖滑阅读,翻页由滚动完成;点击不跳页,只显隐工具栏。 guard !usesWidthFitVerticalScroll else { toggleToolbars() return } if isToolViewVisible { toggleToolbars() } else if point.x < bounds.width / 3 { transitionToPage(pageNum: currentPage - (usesLandscapeSpread ? 2 : 1), animated: true) } else if point.x > bounds.width * 2 / 3 { transitionToPage(pageNum: currentPage + (usesLandscapeSpread ? 2 : 1), animated: true) } else { toggleToolbars() } } } extension RDPDFReaderView: UIPageViewControllerDataSource, UIPageViewControllerDelegate { public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let child = viewController as? RDPDFReaderPageChildViewController else { return nil } let page = child.page - 1 guard page >= 0 else { return nil } return RDPDFReaderPageChildViewController(contentView: makeIndividualPageView(for: page), page: page) } public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let child = viewController as? RDPDFReaderPageChildViewController else { return nil } let page = child.page + 1 guard page < pageCount() else { return nil } return RDPDFReaderPageChildViewController(contentView: makeIndividualPageView(for: page), page: page) } public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { guard completed, let child = pageViewController.viewControllers?.first as? RDPDFReaderPageChildViewController else { return } currentPage = child.page updateNativeCurlBookFrame() } }