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>
This commit is contained in:
parent
72c1f8d89c
commit
fed34da246
239
ReadViewDemo/ReadViewDemo/PDFDemoAnnotationUI.swift
Normal file
239
ReadViewDemo/ReadViewDemo/PDFDemoAnnotationUI.swift
Normal file
@ -0,0 +1,239 @@
|
||||
import UIKit
|
||||
import RDPDFReaderView
|
||||
|
||||
/// 图片型 PDF 的注释输入页。文字和区域标注共用同一编辑体验。
|
||||
final class PDFDemoAnnotationEditorViewController: UIViewController, UITextViewDelegate {
|
||||
private let quote: String
|
||||
private let initialNote: String?
|
||||
private let onSave: (String?) -> Void
|
||||
private let textView = UITextView()
|
||||
private let countLabel = UILabel()
|
||||
|
||||
init(quote: String, initialNote: String? = nil, onSave: @escaping (String?) -> Void) {
|
||||
self.quote = quote
|
||||
self.initialNote = initialNote
|
||||
self.onSave = onSave
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
title = "添加注释"
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
let isDarkMode = traitCollection.userInterfaceStyle == .dark
|
||||
view.backgroundColor = isDarkMode ? UIColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1) : .systemBackground
|
||||
view.accessibilityIdentifier = "pdf.reader.annotation.editor"
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancelAction))
|
||||
navigationItem.leftBarButtonItem?.accessibilityIdentifier = "pdf.reader.annotation.cancel"
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "保存", style: .done, target: self, action: #selector(saveAction))
|
||||
navigationItem.rightBarButtonItem?.accessibilityIdentifier = "pdf.reader.annotation.save"
|
||||
navigationController?.navigationBar.tintColor = .systemBlue
|
||||
|
||||
let quoteCard = UIView()
|
||||
let rail = UIView()
|
||||
let quoteLabel = UILabel()
|
||||
let textCard = UIView()
|
||||
[quoteCard, rail, quoteLabel, textCard, textView, countLabel].forEach {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
view.addSubview(quoteCard)
|
||||
quoteCard.addSubview(rail)
|
||||
quoteCard.addSubview(quoteLabel)
|
||||
view.addSubview(textCard)
|
||||
textCard.addSubview(textView)
|
||||
textCard.addSubview(countLabel)
|
||||
|
||||
let contentBgColor = isDarkMode ? UIColor(red: 0.13, green: 0.13, blue: 0.14, alpha: 1) : UIColor(red: 0.97, green: 0.97, blue: 0.99, alpha: 1)
|
||||
let textColor = isDarkMode ? UIColor(red: 0.92, green: 0.92, blue: 0.96, alpha: 1) : .label
|
||||
|
||||
quoteCard.backgroundColor = contentBgColor.withAlphaComponent(0.78)
|
||||
quoteCard.layer.cornerRadius = 16
|
||||
rail.backgroundColor = UIColor(red: 0.94, green: 0.63, blue: 0.18, alpha: 1)
|
||||
rail.layer.cornerRadius = 2
|
||||
quoteLabel.text = quote
|
||||
quoteLabel.numberOfLines = 5
|
||||
quoteLabel.font = UIFont.systemFont(ofSize: 16)
|
||||
quoteLabel.textColor = textColor.withAlphaComponent(0.85)
|
||||
|
||||
textCard.backgroundColor = contentBgColor
|
||||
textCard.layer.cornerRadius = 18
|
||||
textCard.layer.shadowColor = UIColor.black.cgColor
|
||||
textCard.layer.shadowOpacity = 0.06
|
||||
textCard.layer.shadowRadius = 18
|
||||
textCard.layer.shadowOffset = CGSize(width: 0, height: 6)
|
||||
textView.text = initialNote
|
||||
textView.delegate = self
|
||||
textView.font = UIFont.systemFont(ofSize: 17)
|
||||
textView.textColor = textColor
|
||||
textView.backgroundColor = .clear
|
||||
textView.accessibilityIdentifier = "pdf.reader.annotation.input"
|
||||
countLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
|
||||
countLabel.textColor = .secondaryLabel
|
||||
countLabel.textAlignment = .right
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
quoteCard.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
|
||||
quoteCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
|
||||
quoteCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
|
||||
rail.leadingAnchor.constraint(equalTo: quoteCard.leadingAnchor, constant: 16),
|
||||
rail.topAnchor.constraint(equalTo: quoteCard.topAnchor, constant: 16),
|
||||
rail.bottomAnchor.constraint(equalTo: quoteCard.bottomAnchor, constant: -16),
|
||||
rail.widthAnchor.constraint(equalToConstant: 4),
|
||||
quoteLabel.leadingAnchor.constraint(equalTo: quoteCard.leadingAnchor, constant: 28),
|
||||
quoteLabel.trailingAnchor.constraint(equalTo: quoteCard.trailingAnchor, constant: -16),
|
||||
quoteLabel.topAnchor.constraint(equalTo: quoteCard.topAnchor, constant: 16),
|
||||
quoteLabel.bottomAnchor.constraint(equalTo: quoteCard.bottomAnchor, constant: -16),
|
||||
|
||||
textCard.topAnchor.constraint(equalTo: quoteCard.bottomAnchor, constant: 20),
|
||||
textCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
|
||||
textCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
|
||||
textCard.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -20),
|
||||
textCard.heightAnchor.constraint(greaterThanOrEqualToConstant: 240),
|
||||
textView.topAnchor.constraint(equalTo: textCard.topAnchor, constant: 16),
|
||||
textView.leadingAnchor.constraint(equalTo: textCard.leadingAnchor, constant: 16),
|
||||
textView.trailingAnchor.constraint(equalTo: textCard.trailingAnchor, constant: -16),
|
||||
textView.bottomAnchor.constraint(equalTo: countLabel.topAnchor, constant: -12),
|
||||
countLabel.leadingAnchor.constraint(equalTo: textCard.leadingAnchor, constant: 16),
|
||||
countLabel.trailingAnchor.constraint(equalTo: textCard.trailingAnchor, constant: -16),
|
||||
countLabel.bottomAnchor.constraint(equalTo: textCard.bottomAnchor, constant: -12)
|
||||
])
|
||||
updateCount()
|
||||
DispatchQueue.main.async { [weak self] in self?.textView.becomeFirstResponder() }
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
if textView.text.count > 1000 { textView.text = String(textView.text.prefix(1000)) }
|
||||
updateCount()
|
||||
}
|
||||
|
||||
private func updateCount() { countLabel.text = "\(textView.text.count)/1000" }
|
||||
|
||||
@objc private func cancelAction() { closeEditor() }
|
||||
|
||||
@objc private func saveAction() {
|
||||
let note = textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
onSave(note.isEmpty ? nil : note)
|
||||
closeEditor()
|
||||
}
|
||||
|
||||
private func closeEditor() {
|
||||
guard let navigationController else {
|
||||
dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
if navigationController.viewControllers.first === self {
|
||||
navigationController.dismiss(animated: true)
|
||||
} else {
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一展示文字高亮、区域高亮和带注释的标注,点击可回到原页。
|
||||
final class PDFDemoAnnotationListViewController: UITableViewController {
|
||||
var onSelectAnnotation: ((RDPDFReaderAnnotation) -> Void)?
|
||||
var onDeleteAnnotation: ((RDPDFReaderAnnotation) -> Void)?
|
||||
var onDone: (() -> Void)?
|
||||
|
||||
private let annotationsProvider: () -> [RDPDFReaderAnnotation]
|
||||
private let filterControl = UISegmentedControl(items: ["全部", "注释", "高亮"])
|
||||
private var annotations: [RDPDFReaderAnnotation] = []
|
||||
|
||||
init(annotationsProvider: @escaping () -> [RDPDFReaderAnnotation]) {
|
||||
self.annotationsProvider = annotationsProvider
|
||||
super.init(style: .plain)
|
||||
title = "笔记"
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.accessibilityIdentifier = "pdf.reader.annotations.panel"
|
||||
tableView.accessibilityIdentifier = "pdf.reader.annotations.table"
|
||||
tableView.tableFooterView = UIView()
|
||||
filterControl.selectedSegmentIndex = 0
|
||||
filterControl.accessibilityIdentifier = "pdf.reader.annotations.filter"
|
||||
filterControl.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
|
||||
navigationItem.titleView = filterControl
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "完成", style: .done, target: self, action: #selector(doneAction))
|
||||
reloadAnnotations()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
reloadAnnotations()
|
||||
}
|
||||
|
||||
private var filteredAnnotations: [RDPDFReaderAnnotation] {
|
||||
switch filterControl.selectedSegmentIndex {
|
||||
case 1: return annotations.filter { !($0.note?.isEmpty ?? true) }
|
||||
case 2: return annotations.filter { $0.note?.isEmpty ?? true }
|
||||
default: return annotations
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadAnnotations() {
|
||||
annotations = annotationsProvider().sorted { $0.updatedAt > $1.updatedAt }
|
||||
tableView.reloadData()
|
||||
updateEmptyState()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { filteredAnnotations.count }
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "annotation") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "annotation")
|
||||
let annotation = filteredAnnotations[indexPath.row]
|
||||
cell.textLabel?.numberOfLines = 2
|
||||
cell.detailTextLabel?.numberOfLines = 2
|
||||
cell.textLabel?.text = annotation.selectedText?.isEmpty == false ? annotation.selectedText : "区域标注"
|
||||
let note = annotation.note?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
cell.detailTextLabel?.text = ["第 \(annotation.pageIndex + 1) 页", note].compactMap { $0 }.joined(separator: " · ")
|
||||
cell.imageView?.image = UIImage(systemName: note?.isEmpty == false ? "note.text" : "highlighter")
|
||||
cell.imageView?.tintColor = UIColor(rdPDFHexString: annotation.color)
|
||||
cell.accessibilityIdentifier = "pdf.reader.annotations.row.\(annotation.id)"
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let annotation = filteredAnnotations[indexPath.row]
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
onSelectAnnotation?(annotation)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { true }
|
||||
|
||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||
guard editingStyle == .delete else { return }
|
||||
let annotation = filteredAnnotations[indexPath.row]
|
||||
onDeleteAnnotation?(annotation)
|
||||
reloadAnnotations()
|
||||
}
|
||||
|
||||
private func updateEmptyState() {
|
||||
guard filteredAnnotations.isEmpty else { tableView.backgroundView = nil; return }
|
||||
let label = UILabel()
|
||||
label.text = filterControl.selectedSegmentIndex == 1 ? "暂无注释" : "暂无标注"
|
||||
label.textAlignment = .center
|
||||
label.textColor = .secondaryLabel
|
||||
label.accessibilityIdentifier = "pdf.reader.annotations.empty"
|
||||
tableView.backgroundView = label
|
||||
}
|
||||
|
||||
@objc private func filterChanged() { reloadAnnotations() }
|
||||
@objc private func doneAction() { onDone?() }
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
convenience init(rdPDFHexString: String) {
|
||||
let hex = rdPDFHexString.replacingOccurrences(of: "#", with: "")
|
||||
let value = UInt32(hex, radix: 16) ?? 0xF4C542
|
||||
self.init(
|
||||
red: CGFloat((value >> 16) & 0xFF) / 255,
|
||||
green: CGFloat((value >> 8) & 0xFF) / 255,
|
||||
blue: CGFloat(value & 0xFF) / 255,
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
194
Sources/RDPDFReaderView/ReaderView/RDPDFReaderView+Paging.swift
Normal file
194
Sources/RDPDFReaderView/ReaderView/RDPDFReaderView+Paging.swift
Normal file
@ -0,0 +1,194 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import UIKit
|
||||
|
||||
/// SDK 标注覆盖层:只负责以比例坐标绘制已保存的高亮。
|
||||
public final class RDPDFReaderHighlightOverlayView: UIView {
|
||||
public var highlights: [RDPDFReaderHighlight] = [] { didSet { setNeedsDisplay() } }
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
isOpaque = false
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
public override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
context.clear(rect)
|
||||
for highlight in highlights {
|
||||
let color = UIColor(hexString: highlight.color)
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(2)
|
||||
let normalizedRect = highlight.normalizedRect
|
||||
let underlineRect = CGRect(x: rect.width * normalizedRect.minX, y: rect.height * normalizedRect.minY, width: rect.width * normalizedRect.width, height: rect.height * normalizedRect.height)
|
||||
let y = underlineRect.maxY - 1
|
||||
context.move(to: CGPoint(x: underlineRect.minX, y: y))
|
||||
context.addLine(to: CGPoint(x: underlineRect.maxX, y: y))
|
||||
context.strokePath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SDK 搜索命中覆盖层。与读者标注共用页面比例坐标体系。
|
||||
public final class RDPDFReaderSearchHighlightOverlayView: UIView {
|
||||
public var matchRects: [CGRect] = [] { didSet { setNeedsDisplay() } }
|
||||
public var activeIndex = -1 { didSet { setNeedsDisplay() } }
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
isOpaque = false
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
public override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
context.clear(rect)
|
||||
for (index, matchRect) in matchRects.enumerated() {
|
||||
let color = index == activeIndex
|
||||
? UIColor(red: 0.14, green: 0.42, blue: 0.95, alpha: 0.34)
|
||||
: UIColor(red: 0.21, green: 0.48, blue: 0.95, alpha: 0.16)
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fill(CGRect(x: rect.width * matchRect.minX, y: rect.height * matchRect.minY, width: rect.width * matchRect.width, height: rect.height * matchRect.height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIColor {
|
||||
convenience init(hexString: String) {
|
||||
let hex = hexString.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: "#", with: "")
|
||||
let value = UInt32(hex, radix: 16) ?? 0
|
||||
self.init(red: CGFloat((value >> 16) & 0xFF) / 255, green: CGFloat((value >> 8) & 0xFF) / 255, blue: CGFloat(value & 0xFF) / 255, alpha: 1)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user