Files
ReadViewSDK/Sources/RDPDFReaderView/ReaderView/RDPDFReaderView+Paging.swift
T
shenleiandClaude Haiku 4.5 fed34da246 unify PDF and EPUB reader: convert highlights to underlines, improve annotation UI, change paging from horizontal to vertical
- Convert PDF highlights from filled rectangles to underlines matching EPUB style
- Update PDF annotation editor with EPUB-style design: gold sidebar rail, semi-transparent background, improved spacing and shadows, dark mode support
- Change PDF page-curl paging from left-right horizontal to up-down vertical: update gesture recognition and tap regions accordingly

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-15 10:01:23 +09:00

195 lines
7.7 KiB
Swift

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