import UIKit /// 页面创建、缩放状态和点击分区。与 EPUB 的 `RDEpubReaderView+ContentAccess` /// 及 Paging 模块保持同一职责边界。 extension RDPDFReaderView { func makePageView(for page: Int) -> UIView { 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 } page.readerSetInternalGesturesEnabled(currentDisplayType != .pageCurl) 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 curlPanGestureRecognizer.isEnabled = currentDisplayType == .pageCurl && isPagingEnabled && !isCurrentPageTextSelectionActive } func transitionCurl(to page: Int, animated: Bool) { if isTransitioning { pendingPage = (page, animated) return } if page == currentPage, curlContentView != nil { synchronizePageState() return } let previousPage = currentPage let nextView = makePageView(for: page) nextView.frame = curlHostView.bounds nextView.autoresizingMask = [.flexibleWidth, .flexibleHeight] let installPage = { [weak self] in guard let self else { return } self.curlContentView?.removeFromSuperview() self.curlHostView.addSubview(nextView) self.curlContentView = nextView self.currentPage = page } guard animated, curlContentView != nil, previousPage != page else { installPage() return } isTransitioning = true curlPanInteraction = .idle let animation: UIView.AnimationOptions = page > previousPage ? .transitionCurlUp : .transitionCurlDown UIView.transition( with: curlHostView, duration: 0.38, options: [animation, .allowAnimatedContent], animations: installPage ) { [weak self] _ in self?.finishCurlTransition() } } @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.y) > abs(translation.x) else { return } let crossedDistance = abs(translation.y) >= max(44, curlHostView.bounds.height * 0.12) let crossedVelocity = abs(velocity.y) >= 450 guard crossedDistance || crossedVelocity else { return } let direction = abs(translation.y) >= 4 ? translation.y : velocity.y transitionToPage(pageNum: startPage + (direction < 0 ? 1 : -1), 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 } if isToolViewVisible { toggleToolbars() } else if point.y < bounds.height / 3 { transitionToPage(pageNum: currentPage - 1, animated: true) } else if point.y > bounds.height * 2 / 3 { transitionToPage(pageNum: currentPage + 1, animated: true) } else { toggleToolbars() } } }