将 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>
This commit is contained in:
shenlei
2026-07-15 18:49:44 +09:00
co-authored by Claude Fable 5
parent a17164d1a9
commit 1422461224
347 changed files with 9912 additions and 3941 deletions
@@ -4,6 +4,23 @@ import UIKit
/// 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)
@@ -12,7 +29,9 @@ extension RDPDFReaderView {
func configureInteraction(for view: UIView) {
guard let page = view as? RDPDFReaderPageInteractable else { return }
page.readerSetInternalGesturesEnabled(currentDisplayType != .pageCurl)
// 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))
@@ -38,49 +57,52 @@ extension RDPDFReaderView {
func updatePagingState() {
let enabled = isPagingEnabled && !isCurrentPageZoomed && !isCurrentPageTextSelectionActive
collectionView.isScrollEnabled = enabled
curlPanGestureRecognizer.isEnabled = currentDisplayType == .pageCurl
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) {
if isTransitioning {
pendingPage = (page, animated)
return
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))
}
if page == currentPage, curlContentView != nil {
synchronizePageState()
return
let direction: UIPageViewController.NavigationDirection = page >= currentPage ? .forward : .reverse
controller.setViewControllers(children, direction: direction, animated: animated) { [weak self] _ in
self?.updateNativeCurlBookFrame()
}
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
currentPage = page
if !animated {
updateNativeCurlBookFrame()
}
}
guard animated, curlContentView != nil, previousPage != page else {
installPage()
return
}
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
}
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()
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)
}
}
}
@@ -133,12 +155,13 @@ extension RDPDFReaderView {
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 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.y) >= 4 ? translation.y : velocity.y
transitionToPage(pageNum: startPage + (direction < 0 ? 1 : -1), animated: true)
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:
@@ -181,14 +204,47 @@ extension RDPDFReaderView {
(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.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 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()
}
}