- 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>
195 lines
7.7 KiB
Swift
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()
|
||
}
|
||
}
|
||
}
|