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