ReadViewSDK/Sources/RDPDFReaderView/ReaderView/RDPDFReaderView+Paging.swift
shenlei 1422461224 将 PDF 成品阅读控制器下沉为 RDPDFReaderView SDK,并修复手机横屏单页连续竖滑
SDK 化收尾:
- 新增 RDPDFReaderView 本地 pod(阅读容器、成品控制器、划线/注释、画笔、
  目录/设置面板、主题与持久化契约),Demo 精简为 PDFKit 页面提供者 + 持久化宿主
- Podfile/工程接入 RDPDFReaderView 与 SnapKit,示例书改用 PDF 阅读器示例文件

手机横屏单页竖滑修复:
- cell 高度改为宽度适配后的纸张高度(sizeForItemAt + 宿主按页提供纵横比),
  纵向超出一屏的内容由外层 collectionView 连续滚动承接,不再被裁掉
- 页面内部不再开启同向嵌套的兜底竖向拖动,修复外层滚动被吞、无法滚到下一页
- 修复 applyContentSize 先评估拖动开关后写 contentSize 的顺序缺陷
- 页码按视口中心命中 cell 计算,补 didEndDragging 兜底;旋转重锚点与
  transitionToPage 改用真实布局位置
- 横竖屏同为竖滑时也重建 cell 并刷新全部可见页的适配配置
- 横屏单页点击不再跳页,只显隐工具栏;翻页交给连续竖滑

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 18:49:44 +09:00

251 lines
11 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}