ReadViewSDK/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swift
shenlei 70133e4e6e refactor(reader): 重构高亮选区绘制架构,对齐 WXRead 实现方案
- 移除 RDEPUBSelectableTextView,改用原生 UITextView
- 新增 NSAttributedString 自定义属性(com.rdreader.highlight/underline)注入高亮
- RDEPUBTextPageRenderView 统一绘制高亮背景、文字和选区
- RDEPUBTextSelectionController 精简,选区矩形传递给 RenderView 绘制
- 新增高亮选区复刻 WXRead 实现方案文档
- UI 测试适配新架构

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 20:57:27 +08:00

98 lines
3.6 KiB
Swift
Raw 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
/// UITextView
/// WXRead UITextView attributed string
final class RDEPUBTextSelectionController: NSObject, UITextViewDelegate {
private var isSelectionFromInteraction = false
private var selectionMenuAnchorRect: CGRect?
/// UI
var onSelectionChanged: ((RDEPUBSelection?) -> Void)?
///
var pageProvider: (() -> RDEPUBTextPage?)?
// MARK: - UITextViewDelegate
func textViewDidChangeSelection(_ textView: UITextView) {
textViewDidChangeSelection(textView, page: pageProvider?())
}
func textViewDidChangeSelection(_ textView: UITextView, page: RDEPUBTextPage?) {
guard !isSelectionFromInteraction else { return }
guard let page else {
onSelectionChanged?(nil)
return
}
let selectedRange = textView.selectedRange
guard selectedRange.location != NSNotFound,
selectedRange.length > 0,
let attributedText = textView.attributedText else {
onSelectionChanged?(nil)
return
}
let source = attributedText.string as NSString
let selectedText = source.substring(with: selectedRange).trimmingCharacters(in: .whitespacesAndNewlines)
guard !selectedText.isEmpty else {
onSelectionChanged?(nil)
return
}
let globalStart = page.pageStartOffset + selectedRange.location
let globalEnd = globalStart + selectedRange.length
let totalLength = max(page.chapterContent.length - 1, 1)
let selection = RDEPUBSelection(
location: RDEPUBLocation(
href: page.href,
progression: Double(globalStart) / Double(totalLength),
lastProgression: Double(max(globalEnd - 1, globalStart)) / Double(totalLength),
fragment: nil
),
text: selectedText,
rangeInfo: RDEPUBTextOffsetRangeInfo(href: page.href, start: globalStart, end: globalEnd).jsonString()
)
onSelectionChanged?(selection)
}
// MARK: -
func clearSelection() {
isSelectionFromInteraction = false
selectionMenuAnchorRect = nil
UIMenuController.shared.setMenuVisible(false, animated: true)
onSelectionChanged?(nil)
}
func clearSelection(textView: UITextView) {
textView.selectedRange = NSRange(location: 0, length: 0)
clearSelection()
}
// MARK: -
func showSelectionMenuIfNeeded(
in hostView: UIView,
textView: UITextView,
interactionController: RDEPUBPageInteractionController
) {
guard textView.selectedRange.length > 0,
let textRange = textView.selectedTextRange else { return }
let rect = textView.firstRect(for: textRange)
guard !rect.isNull, !rect.isEmpty else { return }
let menuRect = hostView.convert(rect, from: textView)
hostView.becomeFirstResponder()
let menuController = UIMenuController.shared
menuController.menuItems = [
UIMenuItem(title: "拷贝", action: #selector(RDEPUBTextContentView.rd_copy(_:))),
UIMenuItem(title: "高亮", action: #selector(RDEPUBTextContentView.rd_highlight(_:))),
UIMenuItem(title: "批注", action: #selector(RDEPUBTextContentView.rd_annotate(_:)))
]
menuController.setTargetRect(menuRect, in: hostView)
menuController.setMenuVisible(true, animated: true)
}
}