将 PDF 注释编辑器、笔记列表、目录/设置面板与面板呈现下沉到 SDK

SDK 新增:
- RDPDFReaderAnnotationEditorViewController:注释输入页(UI 与 EPUB 编辑器一致)
- RDPDFReaderAnnotationListViewController:笔记列表(全部/注释/高亮筛选、滑动删除)
- RDPDFReaderNavigationPanelViewController:目录/书签/缩略图面板容器
- RDPDFReaderSettingsPanelViewController:亮度/翻页方式/主题设置面板
- RDPDFReaderPanelPresenter + 转场/呈现控制器:底部面板统一呈现,
  转场代理生命周期由被呈现控制器持有,宿主无需保存引用
- RDPDFReaderThemeOption:SDK 自有主题模型,面板与编辑器不再依赖 EPUB 主题类型
- UIColor(rdPDFHexString:) 公开,并消除覆盖层内的私有重复实现

Demo 侧:
- 删除 PDFDemoAnnotationUI.swift 整个文件
- 删除 PDFDemoNavigationViewController / PDFDemoSettingsViewController /
  面板转场三个类(约 410 行),改为调用 SDK 组件
- ThemePreset 增加到 RDPDFReaderThemeOption 的映射

无障碍标识全部保留,UI 自动化不受影响。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
shenlei 2026-07-15 10:51:51 +09:00
parent 1bde945d36
commit a17164d1a9
8 changed files with 644 additions and 557 deletions

View File

@ -58,6 +58,20 @@ final class PDFDemoReaderViewController: UIViewController, RDPDFReaderDataSource
case .dark: return .dark
}
}
/// 宿 SDK SDK
var option: RDPDFReaderThemeOption {
let theme = self.theme
return RDPDFReaderThemeOption(
identifier: rawValue,
title: title,
contentBackgroundColor: theme.contentBackgroundColor,
contentTextColor: theme.contentTextColor,
toolBackgroundColor: theme.toolBackgroundColor,
toolControlTextColor: theme.toolControlTextColor,
toolLineColor: theme.toolLineColor
)
}
}
private enum OCRPageState: String {
@ -107,7 +121,6 @@ final class PDFDemoReaderViewController: UIViewController, RDPDFReaderDataSource
private weak var bottomToolbar: RDPDFReaderKitBottomToolView?
private var bookmarkedPages = Set<Int>()
private var currentThemePreset: ThemePreset = .light
private var activePanelTransitionDelegate: PDFDemoPanelTransitionDelegate?
private var activeSelection: RDPDFReaderImageTextSelection?
private var copiedText: String?
private var annotationPersistenceError: String?
@ -313,10 +326,11 @@ final class PDFDemoReaderViewController: UIViewController, RDPDFReaderDataSource
private func showAnnotationEditor(for selection: RDPDFReaderImageTextSelection, pageIndex: Int) {
let quote = selection.text?.trimmingCharacters(in: .whitespacesAndNewlines)
let editor = PDFDemoAnnotationEditorViewController(
let editor = RDPDFReaderAnnotationEditorViewController(
quote: quote?.isEmpty == false ? quote! : "区域标注",
theme: currentThemePreset.option,
onSave: { [weak self] note in
self?.addAnnotation(from: selection, pageIndex: pageIndex, color: "#F8E16C", note: note)
self?.addAnnotation(from: selection, pageIndex: pageIndex, color: RDPDFReaderPageView.defaultHighlightColor, note: note)
}
)
let navigationController = UINavigationController(rootViewController: editor)
@ -324,9 +338,10 @@ final class PDFDemoReaderViewController: UIViewController, RDPDFReaderDataSource
}
private func showAnnotationEditor(editing annotation: RDPDFReaderAnnotation) {
let editor = PDFDemoAnnotationEditorViewController(
let editor = RDPDFReaderAnnotationEditorViewController(
quote: annotation.selectedText?.isEmpty == false ? annotation.selectedText! : "区域标注",
initialNote: annotation.note,
theme: currentThemePreset.option,
onSave: { [weak self] note in
guard let self else { return }
var updated = annotation
@ -538,7 +553,7 @@ final class PDFDemoReaderViewController: UIViewController, RDPDFReaderDataSource
private func showNavigationSheet() {
let bookmarks = bookmarkedPages.sorted().map { RDPDFReaderBookmark(pageIndex: $0, title: "\($0 + 1)") }
let controller = PDFDemoNavigationViewController(
let controller = RDPDFReaderNavigationPanelViewController(
outlineItems: outlineItems(),
bookmarks: bookmarks,
totalPages: document.pageCount,
@ -582,20 +597,24 @@ final class PDFDemoReaderViewController: UIViewController, RDPDFReaderDataSource
}
private func showSettingsPanel() {
let controller = PDFDemoSettingsViewController(
let controller = RDPDFReaderSettingsPanelViewController(
displayType: readerView.currentDisplayType,
brightness: UIScreen.main.brightness,
themePreset: currentThemePreset
themes: ThemePreset.allCases.map(\.option),
selectedThemeIdentifier: currentThemePreset.rawValue
)
controller.onBrightnessChange = { UIScreen.main.brightness = $0 }
controller.onDisplayTypeChange = { [weak self] in self?.applyDisplayType($0) }
controller.onThemeChange = { [weak self] in self?.applyTheme($0, refreshCurrentPage: true) }
controller.onThemeChange = { [weak self] option in
guard let preset = ThemePreset(rawValue: option.identifier) else { return }
self?.applyTheme(preset, refreshCurrentPage: true)
}
let navigationController = UINavigationController(rootViewController: controller)
presentPanel(navigationController, layout: .settings)
}
private func showAnnotationsPanel() {
let controller = PDFDemoAnnotationListViewController { [weak self] in
let controller = RDPDFReaderAnnotationListViewController { [weak self] in
self?.annotations() ?? []
}
let navigationController = UINavigationController(rootViewController: controller)
@ -624,17 +643,10 @@ final class PDFDemoReaderViewController: UIViewController, RDPDFReaderDataSource
private func presentPanel(
_ controller: UINavigationController,
layout: PDFDemoPanelLayout,
layout: RDPDFReaderPanelLayout,
hidesNavigationBar: Bool = true
) {
let transitionDelegate = PDFDemoPanelTransitionDelegate(layout: layout)
activePanelTransitionDelegate = transitionDelegate
controller.setNavigationBarHidden(hidesNavigationBar, animated: false)
controller.view.backgroundColor = .clear
controller.view.isOpaque = false
controller.modalPresentationStyle = .custom
controller.transitioningDelegate = transitionDelegate
present(controller, animated: true)
RDPDFReaderPanelPresenter.present(controller, from: self, layout: layout, hidesNavigationBar: hidesNavigationBar)
}
private func applyDisplayType(_ type: RDPDFReaderView.DisplayType) {
@ -811,415 +823,3 @@ extension PDFDemoReaderViewController: RDPDFReaderPageViewDelegate {
}
}
private final class PDFDemoNavigationViewController: UIViewController {
private let handleView = UIView()
private let closeButton = UIButton(type: .system)
private let navigationView = RDPDFReaderNavigationView()
private let outlineItems: [RDPDFReaderOutlineItem]
private let bookmarks: [RDPDFReaderBookmark]
private let totalPages: Int
private let thumbnailProvider: (Int, CGSize, @escaping (UIImage?) -> Void) -> Void
private let onSelectPage: (Int) -> Void
init(
outlineItems: [RDPDFReaderOutlineItem],
bookmarks: [RDPDFReaderBookmark],
totalPages: Int,
thumbnailProvider: @escaping (Int, CGSize, @escaping (UIImage?) -> Void) -> Void,
onSelectPage: @escaping (Int) -> Void
) {
self.outlineItems = outlineItems
self.bookmarks = bookmarks
self.totalPages = totalPages
self.thumbnailProvider = thumbnailProvider
self.onSelectPage = onSelectPage
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.layer.cornerRadius = 18
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.layer.masksToBounds = true
view.accessibilityIdentifier = "pdf.reader.navigation.panel"
[handleView, closeButton, navigationView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
handleView.layer.cornerRadius = 3
handleView.backgroundColor = UIColor.label.withAlphaComponent(0.35)
closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
closeButton.tintColor = .label
closeButton.accessibilityIdentifier = "pdf.reader.navigation.done"
closeButton.addTarget(self, action: #selector(doneAction), for: .touchUpInside)
NSLayoutConstraint.activate([
handleView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
handleView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
handleView.widthAnchor.constraint(equalToConstant: 42),
handleView.heightAnchor.constraint(equalToConstant: 5),
closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
closeButton.centerYAnchor.constraint(equalTo: handleView.centerYAnchor, constant: 20),
closeButton.widthAnchor.constraint(equalToConstant: 32),
closeButton.heightAnchor.constraint(equalToConstant: 32),
navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
navigationView.topAnchor.constraint(equalTo: handleView.bottomAnchor, constant: 14),
navigationView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
navigationView.outlineItems = outlineItems
navigationView.bookmarks = bookmarks
navigationView.configure(tab: .catalog, totalPages: totalPages, thumbnailProvider: thumbnailProvider)
navigationView.itemTapHandler = { [weak self] page in
self?.onSelectPage(page)
self?.dismiss(animated: true)
}
}
@objc private func doneAction() {
dismiss(animated: true)
}
}
private final class PDFDemoSettingsViewController: UIViewController {
var onBrightnessChange: ((CGFloat) -> Void)?
var onDisplayTypeChange: ((RDPDFReaderView.DisplayType) -> Void)?
var onThemeChange: ((PDFDemoReaderViewController.ThemePreset) -> Void)?
private let handleView = UIView()
private let closeButton = UIButton(type: .system)
private let scrollView = UIScrollView()
private let contentView = UIView()
private let brightnessSlider = UISlider()
private let displayTypeControl = UISegmentedControl(items: ["仿真", "横滑", "竖滑"])
private let themeStack = UIStackView()
private var themeButtons: [UIButton] = []
private var currentDisplayType: RDPDFReaderView.DisplayType
private var currentThemePreset: PDFDemoReaderViewController.ThemePreset
init(
displayType: RDPDFReaderView.DisplayType,
brightness: CGFloat,
themePreset: PDFDemoReaderViewController.ThemePreset
) {
currentDisplayType = displayType
currentThemePreset = themePreset
super.init(nibName: nil, bundle: nil)
brightnessSlider.value = Float(brightness)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
updateDisplayTypeSelection()
applyTheme(currentThemePreset.theme)
}
private func setupViews() {
view.backgroundColor = .systemBackground
view.layer.cornerRadius = 18
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.layer.masksToBounds = true
view.accessibilityIdentifier = "epub.reader.settings.panel"
scrollView.accessibilityIdentifier = "epub.reader.settings.scroll"
closeButton.accessibilityIdentifier = "epub.reader.settings.done"
brightnessSlider.accessibilityIdentifier = "epub.reader.settings.brightness"
displayTypeControl.accessibilityIdentifier = "epub.reader.settings.displayType"
[handleView, closeButton, scrollView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
handleView.layer.cornerRadius = 3
closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
closeButton.addTarget(self, action: #selector(doneAction), for: .touchUpInside)
brightnessSlider.minimumValue = 0
brightnessSlider.maximumValue = 1
brightnessSlider.addTarget(self, action: #selector(brightnessChanged(_:)), for: .valueChanged)
displayTypeControl.addTarget(self, action: #selector(displayTypeChanged(_:)), for: .valueChanged)
themeStack.axis = .horizontal
themeStack.distribution = .fillEqually
themeStack.spacing = 14
PDFDemoReaderViewController.ThemePreset.allCases.forEach { preset in
let button = UIButton(type: .system)
button.tag = preset.rawValue
button.backgroundColor = preset.theme.contentBackgroundColor
button.layer.cornerRadius = 15
button.layer.borderWidth = 1
button.accessibilityIdentifier = "epub.reader.settings.theme.\(preset.rawValue)"
button.accessibilityLabel = preset.title
button.addTarget(self, action: #selector(themeButtonAction(_:)), for: .touchUpInside)
themeButtons.append(button)
themeStack.addArrangedSubview(button)
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
}
let brightnessLabel = makeLabel("亮度")
let displayTypeLabel = makeLabel("翻页方式")
let themeLabel = makeLabel("主题")
let dividerOne = makeDivider()
let dividerTwo = makeDivider()
[brightnessLabel, displayTypeLabel, themeLabel, brightnessSlider, displayTypeControl, themeStack, dividerOne, dividerTwo].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview($0)
}
NSLayoutConstraint.activate([
handleView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
handleView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
handleView.widthAnchor.constraint(equalToConstant: 42),
handleView.heightAnchor.constraint(equalToConstant: 5),
closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
closeButton.centerYAnchor.constraint(equalTo: handleView.centerYAnchor, constant: 20),
closeButton.widthAnchor.constraint(equalToConstant: 32),
closeButton.heightAnchor.constraint(equalToConstant: 32),
scrollView.topAnchor.constraint(equalTo: handleView.bottomAnchor, constant: 14),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
brightnessLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
brightnessLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
brightnessSlider.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
brightnessSlider.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
brightnessSlider.topAnchor.constraint(equalTo: brightnessLabel.bottomAnchor, constant: 8),
dividerOne.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
dividerOne.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
dividerOne.topAnchor.constraint(equalTo: brightnessSlider.bottomAnchor, constant: 14),
displayTypeLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
displayTypeLabel.topAnchor.constraint(equalTo: dividerOne.bottomAnchor, constant: 14),
displayTypeControl.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
displayTypeControl.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
displayTypeControl.topAnchor.constraint(equalTo: displayTypeLabel.bottomAnchor, constant: 8),
displayTypeControl.heightAnchor.constraint(equalToConstant: 32),
dividerTwo.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
dividerTwo.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
dividerTwo.topAnchor.constraint(equalTo: displayTypeControl.bottomAnchor, constant: 14),
themeLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
themeLabel.topAnchor.constraint(equalTo: dividerTwo.bottomAnchor, constant: 14),
themeStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
themeStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
themeStack.topAnchor.constraint(equalTo: themeLabel.bottomAnchor, constant: 12),
themeStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
}
private func makeLabel(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 13, weight: .semibold)
return label
}
private func makeDivider() -> UIView {
let divider = UIView()
divider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
return divider
}
private func updateDisplayTypeSelection() {
let selectedIndex: Int
switch currentDisplayType {
case .pageCurl:
selectedIndex = 0
case .horizontalScroll:
selectedIndex = 1
case .verticalScroll:
selectedIndex = 2
case .horizontalCoverScroll:
selectedIndex = 1
}
displayTypeControl.selectedSegmentIndex = selectedIndex
displayTypeControl.accessibilityValue = displayTypeControl.titleForSegment(at: selectedIndex)
}
private func applyTheme(_ theme: RDEPUBReaderTheme) {
view.backgroundColor = theme.toolBackgroundColor.withAlphaComponent(0.98)
contentView.backgroundColor = .clear
handleView.backgroundColor = theme.toolControlTextColor.withAlphaComponent(0.45)
closeButton.tintColor = theme.toolControlTextColor
view.subviews.compactMap { $0 as? UILabel }.forEach { $0.textColor = theme.toolControlTextColor }
contentView.subviews.compactMap { $0 as? UILabel }.forEach { $0.textColor = theme.toolControlTextColor }
displayTypeControl.selectedSegmentTintColor = .systemBlue
let normalAttrs: [NSAttributedString.Key: Any] = [.foregroundColor: theme.toolControlTextColor]
let selectedAttrs: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white]
displayTypeControl.setTitleTextAttributes(normalAttrs, for: .normal)
displayTypeControl.setTitleTextAttributes(selectedAttrs, for: .selected)
displayTypeControl.backgroundColor = theme.toolControlTextColor.withAlphaComponent(0.10)
brightnessSlider.minimumTrackTintColor = .systemBlue
brightnessSlider.maximumTrackTintColor = theme.toolControlTextColor.withAlphaComponent(0.22)
contentView.subviews.filter { $0 !== brightnessSlider && $0 !== displayTypeControl && !($0 is UILabel) && !($0 is UIStackView) }
.forEach { view in
if view !== handleView && view !== closeButton {
view.backgroundColor = theme.toolLineColor
}
}
updateDisplayTypeSelection()
updateThemeSelection()
}
private func updateThemeSelection() {
themeButtons.forEach { button in
let isSelected = button.tag == currentThemePreset.rawValue
button.layer.borderWidth = isSelected ? 2.5 : 1
button.layer.borderColor = isSelected ? UIColor.systemBlue.cgColor : UIColor.white.withAlphaComponent(0.28).cgColor
button.accessibilityValue = isSelected ? "selected" : "unselected"
}
}
@objc private func doneAction() {
dismiss(animated: true)
}
@objc private func brightnessChanged(_ sender: UISlider) {
onBrightnessChange?(CGFloat(sender.value))
}
@objc private func displayTypeChanged(_ sender: UISegmentedControl) {
let type: RDPDFReaderView.DisplayType
switch sender.selectedSegmentIndex {
case 0:
type = .pageCurl
case 1:
type = .horizontalScroll
case 2:
type = .verticalScroll
default:
type = .pageCurl
}
currentDisplayType = type
updateDisplayTypeSelection()
onDisplayTypeChange?(type)
}
@objc private func themeButtonAction(_ sender: UIButton) {
guard let preset = PDFDemoReaderViewController.ThemePreset(rawValue: sender.tag) else { return }
currentThemePreset = preset
applyTheme(preset.theme)
onThemeChange?(preset)
}
}
private enum PDFDemoPanelLayout {
case settings
case navigation
}
private final class PDFDemoPanelTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
private let layout: PDFDemoPanelLayout
init(layout: PDFDemoPanelLayout) {
self.layout = layout
}
func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
PDFDemoPanelPresentationController(
presentedViewController: presented,
presenting: presenting ?? source,
layout: layout
)
}
}
private final class PDFDemoPanelPresentationController: UIPresentationController {
private let dimmingView = UIControl()
private let layout: PDFDemoPanelLayout
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, layout: PDFDemoPanelLayout) {
self.layout = layout
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView else { return .zero }
let bounds = containerView.bounds
let isLandscape = bounds.width > bounds.height
if isLandscape {
let safeBounds = bounds.inset(by: containerView.safeAreaInsets)
let width = min(390, max(300, bounds.width * 0.32))
let height = min(layout == .settings ? 340 : 520, safeBounds.height - 32)
return CGRect(
x: safeBounds.maxX - width - 16,
y: safeBounds.midY - height / 2,
width: width,
height: height
)
}
let height: CGFloat
switch layout {
case .settings:
height = min(350, max(280, bounds.height * 0.40))
case .navigation:
height = min(bounds.height - 16, max(420, bounds.height * 0.72))
}
return CGRect(x: 0, y: bounds.maxY - height, width: bounds.width, height: height)
}
override func presentationTransitionWillBegin() {
guard let containerView else { return }
dimmingView.backgroundColor = UIColor.black.withAlphaComponent(0.18)
dimmingView.alpha = 0
dimmingView.addTarget(self, action: #selector(dismissPresentedController), for: .touchUpInside)
dimmingView.frame = containerView.bounds
containerView.insertSubview(dimmingView, at: 0)
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1
})
}
override func dismissalTransitionWillBegin() {
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
dimmingView.frame = containerView?.bounds ?? .zero
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
super.dismissalTransitionDidEnd(completed)
if completed { dimmingView.removeFromSuperview() }
}
@objc private func dismissPresentedController() {
presentedViewController.dismiss(animated: true)
}
}

View File

@ -1,28 +1,37 @@
import UIKit
import RDPDFReaderView
/// PDF
final class PDFDemoAnnotationEditorViewController: UIViewController, UITextViewDelegate {
/// PDF
/// UI EPUB `RDEPUBAnnotationEditorViewController`
public final class RDPDFReaderAnnotationEditorViewController: UIViewController, UITextViewDelegate {
private let quote: String
private let initialNote: String?
private let theme: RDPDFReaderThemeOption?
private let onSave: (String?) -> Void
private let textView = UITextView()
private let countLabel = UILabel()
init(quote: String, initialNote: String? = nil, onSave: @escaping (String?) -> Void) {
public init(
quote: String,
initialNote: String? = nil,
theme: RDPDFReaderThemeOption? = nil,
onSave: @escaping (String?) -> Void
) {
self.quote = quote
self.initialNote = initialNote
self.theme = theme
self.onSave = onSave
super.init(nibName: nil, bundle: nil)
title = "添加注释"
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
public 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
let backgroundColor = theme?.toolBackgroundColor
?? (isDarkMode ? UIColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1) : .systemBackground)
view.backgroundColor = backgroundColor
view.accessibilityIdentifier = "pdf.reader.annotation.editor"
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancelAction))
navigationItem.leftBarButtonItem?.accessibilityIdentifier = "pdf.reader.annotation.cancel"
@ -44,8 +53,10 @@ final class PDFDemoAnnotationEditorViewController: UIViewController, UITextViewD
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
let contentBgColor = theme?.contentBackgroundColor
?? (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 = theme?.contentTextColor
?? (isDarkMode ? UIColor(red: 0.92, green: 0.92, blue: 0.96, alpha: 1) : .label)
quoteCard.backgroundColor = contentBgColor.withAlphaComponent(0.78)
quoteCard.layer.cornerRadius = 16
@ -102,7 +113,7 @@ final class PDFDemoAnnotationEditorViewController: UIViewController, UITextViewD
DispatchQueue.main.async { [weak self] in self?.textView.becomeFirstResponder() }
}
func textViewDidChange(_ textView: UITextView) {
public func textViewDidChange(_ textView: UITextView) {
if textView.text.count > 1000 { textView.text = String(textView.text.prefix(1000)) }
updateCount()
}
@ -129,111 +140,3 @@ final class PDFDemoAnnotationEditorViewController: UIViewController, UITextViewD
}
}
}
///
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
)
}
}

View File

@ -0,0 +1,97 @@
import UIKit
/// 线
/// `annotationsProvider` /宿
public final class RDPDFReaderAnnotationListViewController: UITableViewController {
public var onSelectAnnotation: ((RDPDFReaderAnnotation) -> Void)?
public var onDeleteAnnotation: ((RDPDFReaderAnnotation) -> Void)?
public var onDone: (() -> Void)?
private let annotationsProvider: () -> [RDPDFReaderAnnotation]
private let filterControl = UISegmentedControl(items: ["全部", "注释", "高亮"])
private var annotations: [RDPDFReaderAnnotation] = []
public init(annotationsProvider: @escaping () -> [RDPDFReaderAnnotation]) {
self.annotationsProvider = annotationsProvider
super.init(style: .plain)
title = "笔记"
}
public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
public 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()
}
public 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()
}
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { filteredAnnotations.count }
public 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
}
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let annotation = filteredAnnotations[indexPath.row]
tableView.deselectRow(at: indexPath, animated: true)
onSelectAnnotation?(annotation)
}
public override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { true }
public 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?() }
}

View File

@ -18,7 +18,7 @@ public final class RDPDFReaderHighlightOverlayView: UIView {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.clear(rect)
for highlight in highlights {
let color = UIColor(hexString: highlight.color)
let color = UIColor(rdPDFHexString: highlight.color)
context.setStrokeColor(color.cgColor)
context.setLineWidth(2)
let normalizedRect = highlight.normalizedRect
@ -57,11 +57,3 @@ public final class RDPDFReaderSearchHighlightOverlayView: UIView {
}
}
}
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)
}
}

View File

@ -0,0 +1,80 @@
import UIKit
/// // `RDPDFReaderNavigationView`
/// `RDPDFReaderPanelPresenter`
public final class RDPDFReaderNavigationPanelViewController: UIViewController {
private let handleView = UIView()
private let closeButton = UIButton(type: .system)
private let navigationView = RDPDFReaderNavigationView()
private let outlineItems: [RDPDFReaderOutlineItem]
private let bookmarks: [RDPDFReaderBookmark]
private let totalPages: Int
private let thumbnailProvider: (Int, CGSize, @escaping (UIImage?) -> Void) -> Void
private let onSelectPage: (Int) -> Void
public init(
outlineItems: [RDPDFReaderOutlineItem],
bookmarks: [RDPDFReaderBookmark],
totalPages: Int,
thumbnailProvider: @escaping (Int, CGSize, @escaping (UIImage?) -> Void) -> Void,
onSelectPage: @escaping (Int) -> Void
) {
self.outlineItems = outlineItems
self.bookmarks = bookmarks
self.totalPages = totalPages
self.thumbnailProvider = thumbnailProvider
self.onSelectPage = onSelectPage
super.init(nibName: nil, bundle: nil)
}
public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.layer.cornerRadius = 18
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.layer.masksToBounds = true
view.accessibilityIdentifier = "pdf.reader.navigation.panel"
[handleView, closeButton, navigationView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
handleView.layer.cornerRadius = 3
handleView.backgroundColor = UIColor.label.withAlphaComponent(0.35)
closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
closeButton.tintColor = .label
closeButton.accessibilityIdentifier = "pdf.reader.navigation.done"
closeButton.addTarget(self, action: #selector(doneAction), for: .touchUpInside)
NSLayoutConstraint.activate([
handleView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
handleView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
handleView.widthAnchor.constraint(equalToConstant: 42),
handleView.heightAnchor.constraint(equalToConstant: 5),
closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
closeButton.centerYAnchor.constraint(equalTo: handleView.centerYAnchor, constant: 20),
closeButton.widthAnchor.constraint(equalToConstant: 32),
closeButton.heightAnchor.constraint(equalToConstant: 32),
navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
navigationView.topAnchor.constraint(equalTo: handleView.bottomAnchor, constant: 14),
navigationView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
navigationView.outlineItems = outlineItems
navigationView.bookmarks = bookmarks
navigationView.configure(tab: .catalog, totalPages: totalPages, thumbnailProvider: thumbnailProvider)
navigationView.itemTapHandler = { [weak self] page in
self?.onSelectPage(page)
self?.dismiss(animated: true)
}
}
@objc private func doneAction() {
dismiss(animated: true)
}
}

View File

@ -0,0 +1,120 @@
import UIKit
import ObjectiveC
/// /
public enum RDPDFReaderPanelLayout {
case settings
case navigation
}
///
/// 宿
public enum RDPDFReaderPanelPresenter {
private static var transitionDelegateKey: UInt8 = 0
public static func present(
_ controller: UINavigationController,
from presenter: UIViewController,
layout: RDPDFReaderPanelLayout,
hidesNavigationBar: Bool = true
) {
let transitionDelegate = RDPDFReaderPanelTransitionDelegate(layout: layout)
objc_setAssociatedObject(controller, &transitionDelegateKey, transitionDelegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
controller.setNavigationBarHidden(hidesNavigationBar, animated: false)
controller.view.backgroundColor = .clear
controller.view.isOpaque = false
controller.modalPresentationStyle = .custom
controller.transitioningDelegate = transitionDelegate
presenter.present(controller, animated: true)
}
}
public final class RDPDFReaderPanelTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
private let layout: RDPDFReaderPanelLayout
public init(layout: RDPDFReaderPanelLayout) {
self.layout = layout
}
public func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
RDPDFReaderPanelPresentationController(
presentedViewController: presented,
presenting: presenting ?? source,
layout: layout
)
}
}
final class RDPDFReaderPanelPresentationController: UIPresentationController {
private let dimmingView = UIControl()
private let layout: RDPDFReaderPanelLayout
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, layout: RDPDFReaderPanelLayout) {
self.layout = layout
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView else { return .zero }
let bounds = containerView.bounds
let isLandscape = bounds.width > bounds.height
if isLandscape {
let safeBounds = bounds.inset(by: containerView.safeAreaInsets)
let width = min(390, max(300, bounds.width * 0.32))
let height = min(layout == .settings ? 340 : 520, safeBounds.height - 32)
return CGRect(
x: safeBounds.maxX - width - 16,
y: safeBounds.midY - height / 2,
width: width,
height: height
)
}
let height: CGFloat
switch layout {
case .settings:
height = min(350, max(280, bounds.height * 0.40))
case .navigation:
height = min(bounds.height - 16, max(420, bounds.height * 0.72))
}
return CGRect(x: 0, y: bounds.maxY - height, width: bounds.width, height: height)
}
override func presentationTransitionWillBegin() {
guard let containerView else { return }
dimmingView.backgroundColor = UIColor.black.withAlphaComponent(0.18)
dimmingView.alpha = 0
dimmingView.addTarget(self, action: #selector(dismissPresentedController), for: .touchUpInside)
dimmingView.frame = containerView.bounds
containerView.insertSubview(dimmingView, at: 0)
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1
})
}
override func dismissalTransitionWillBegin() {
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
dimmingView.frame = containerView?.bounds ?? .zero
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
super.dismissalTransitionDidEnd(completed)
if completed { dimmingView.removeFromSuperview() }
}
@objc private func dismissPresentedController() {
presentedViewController.dismiss(animated: true)
}
}

View File

@ -0,0 +1,249 @@
import UIKit
///
/// 宿 `RDPDFReaderThemeOption`
/// SDK 宿
public final class RDPDFReaderSettingsPanelViewController: UIViewController {
public var onBrightnessChange: ((CGFloat) -> Void)?
public var onDisplayTypeChange: ((RDPDFReaderView.DisplayType) -> Void)?
public var onThemeChange: ((RDPDFReaderThemeOption) -> Void)?
private let handleView = UIView()
private let closeButton = UIButton(type: .system)
private let scrollView = UIScrollView()
private let contentView = UIView()
private let brightnessSlider = UISlider()
private let displayTypeControl = UISegmentedControl(items: ["仿真", "横滑", "竖滑"])
private let themeStack = UIStackView()
private var themeButtons: [UIButton] = []
private let themes: [RDPDFReaderThemeOption]
private var currentDisplayType: RDPDFReaderView.DisplayType
private var currentTheme: RDPDFReaderThemeOption
public init(
displayType: RDPDFReaderView.DisplayType,
brightness: CGFloat,
themes: [RDPDFReaderThemeOption],
selectedThemeIdentifier: Int
) {
precondition(!themes.isEmpty, "至少需要一个主题选项")
self.themes = themes
currentDisplayType = displayType
currentTheme = themes.first { $0.identifier == selectedThemeIdentifier } ?? themes[0]
super.init(nibName: nil, bundle: nil)
brightnessSlider.value = Float(brightness)
}
public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
public override func viewDidLoad() {
super.viewDidLoad()
setupViews()
updateDisplayTypeSelection()
applyTheme(currentTheme)
}
private func setupViews() {
view.backgroundColor = .systemBackground
view.layer.cornerRadius = 18
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.layer.masksToBounds = true
view.accessibilityIdentifier = "epub.reader.settings.panel"
scrollView.accessibilityIdentifier = "epub.reader.settings.scroll"
closeButton.accessibilityIdentifier = "epub.reader.settings.done"
brightnessSlider.accessibilityIdentifier = "epub.reader.settings.brightness"
displayTypeControl.accessibilityIdentifier = "epub.reader.settings.displayType"
[handleView, closeButton, scrollView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
handleView.layer.cornerRadius = 3
closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
closeButton.addTarget(self, action: #selector(doneAction), for: .touchUpInside)
brightnessSlider.minimumValue = 0
brightnessSlider.maximumValue = 1
brightnessSlider.addTarget(self, action: #selector(brightnessChanged(_:)), for: .valueChanged)
displayTypeControl.addTarget(self, action: #selector(displayTypeChanged(_:)), for: .valueChanged)
themeStack.axis = .horizontal
themeStack.distribution = .fillEqually
themeStack.spacing = 14
themes.forEach { theme in
let button = UIButton(type: .system)
button.tag = theme.identifier
button.backgroundColor = theme.contentBackgroundColor
button.layer.cornerRadius = 15
button.layer.borderWidth = 1
button.accessibilityIdentifier = "epub.reader.settings.theme.\(theme.identifier)"
button.accessibilityLabel = theme.title
button.addTarget(self, action: #selector(themeButtonAction(_:)), for: .touchUpInside)
themeButtons.append(button)
themeStack.addArrangedSubview(button)
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
}
let brightnessLabel = makeLabel("亮度")
let displayTypeLabel = makeLabel("翻页方式")
let themeLabel = makeLabel("主题")
let dividerOne = makeDivider()
let dividerTwo = makeDivider()
[brightnessLabel, displayTypeLabel, themeLabel, brightnessSlider, displayTypeControl, themeStack, dividerOne, dividerTwo].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview($0)
}
NSLayoutConstraint.activate([
handleView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
handleView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
handleView.widthAnchor.constraint(equalToConstant: 42),
handleView.heightAnchor.constraint(equalToConstant: 5),
closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
closeButton.centerYAnchor.constraint(equalTo: handleView.centerYAnchor, constant: 20),
closeButton.widthAnchor.constraint(equalToConstant: 32),
closeButton.heightAnchor.constraint(equalToConstant: 32),
scrollView.topAnchor.constraint(equalTo: handleView.bottomAnchor, constant: 14),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
brightnessLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
brightnessLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
brightnessSlider.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
brightnessSlider.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
brightnessSlider.topAnchor.constraint(equalTo: brightnessLabel.bottomAnchor, constant: 8),
dividerOne.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
dividerOne.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
dividerOne.topAnchor.constraint(equalTo: brightnessSlider.bottomAnchor, constant: 14),
displayTypeLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
displayTypeLabel.topAnchor.constraint(equalTo: dividerOne.bottomAnchor, constant: 14),
displayTypeControl.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
displayTypeControl.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
displayTypeControl.topAnchor.constraint(equalTo: displayTypeLabel.bottomAnchor, constant: 8),
displayTypeControl.heightAnchor.constraint(equalToConstant: 32),
dividerTwo.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
dividerTwo.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
dividerTwo.topAnchor.constraint(equalTo: displayTypeControl.bottomAnchor, constant: 14),
themeLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
themeLabel.topAnchor.constraint(equalTo: dividerTwo.bottomAnchor, constant: 14),
themeStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
themeStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
themeStack.topAnchor.constraint(equalTo: themeLabel.bottomAnchor, constant: 12),
themeStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
}
private func makeLabel(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 13, weight: .semibold)
return label
}
private func makeDivider() -> UIView {
let divider = UIView()
divider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
return divider
}
private func updateDisplayTypeSelection() {
let selectedIndex: Int
switch currentDisplayType {
case .pageCurl:
selectedIndex = 0
case .horizontalScroll:
selectedIndex = 1
case .verticalScroll:
selectedIndex = 2
case .horizontalCoverScroll:
selectedIndex = 1
}
displayTypeControl.selectedSegmentIndex = selectedIndex
displayTypeControl.accessibilityValue = displayTypeControl.titleForSegment(at: selectedIndex)
}
private func applyTheme(_ theme: RDPDFReaderThemeOption) {
view.backgroundColor = theme.toolBackgroundColor.withAlphaComponent(0.98)
contentView.backgroundColor = .clear
handleView.backgroundColor = theme.toolControlTextColor.withAlphaComponent(0.45)
closeButton.tintColor = theme.toolControlTextColor
view.subviews.compactMap { $0 as? UILabel }.forEach { $0.textColor = theme.toolControlTextColor }
contentView.subviews.compactMap { $0 as? UILabel }.forEach { $0.textColor = theme.toolControlTextColor }
displayTypeControl.selectedSegmentTintColor = .systemBlue
let normalAttrs: [NSAttributedString.Key: Any] = [.foregroundColor: theme.toolControlTextColor]
let selectedAttrs: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white]
displayTypeControl.setTitleTextAttributes(normalAttrs, for: .normal)
displayTypeControl.setTitleTextAttributes(selectedAttrs, for: .selected)
displayTypeControl.backgroundColor = theme.toolControlTextColor.withAlphaComponent(0.10)
brightnessSlider.minimumTrackTintColor = .systemBlue
brightnessSlider.maximumTrackTintColor = theme.toolControlTextColor.withAlphaComponent(0.22)
contentView.subviews.filter { $0 !== brightnessSlider && $0 !== displayTypeControl && !($0 is UILabel) && !($0 is UIStackView) }
.forEach { view in
if view !== handleView && view !== closeButton {
view.backgroundColor = theme.toolLineColor
}
}
updateDisplayTypeSelection()
updateThemeSelection()
}
private func updateThemeSelection() {
themeButtons.forEach { button in
let isSelected = button.tag == currentTheme.identifier
button.layer.borderWidth = isSelected ? 2.5 : 1
button.layer.borderColor = isSelected ? UIColor.systemBlue.cgColor : UIColor.white.withAlphaComponent(0.28).cgColor
button.accessibilityValue = isSelected ? "selected" : "unselected"
}
}
@objc private func doneAction() {
dismiss(animated: true)
}
@objc private func brightnessChanged(_ sender: UISlider) {
onBrightnessChange?(CGFloat(sender.value))
}
@objc private func displayTypeChanged(_ sender: UISegmentedControl) {
let type: RDPDFReaderView.DisplayType
switch sender.selectedSegmentIndex {
case 0:
type = .pageCurl
case 1:
type = .horizontalScroll
case 2:
type = .verticalScroll
default:
type = .pageCurl
}
currentDisplayType = type
updateDisplayTypeSelection()
onDisplayTypeChange?(type)
}
@objc private func themeButtonAction(_ sender: UIButton) {
guard let theme = themes.first(where: { $0.identifier == sender.tag }) else { return }
currentTheme = theme
applyTheme(theme)
onThemeChange?(theme)
}
}

View File

@ -0,0 +1,46 @@
import UIKit
/// PDF SDK 宿
/// 宿 SDK
public struct RDPDFReaderThemeOption: Equatable {
/// 宿 rawValue
public let identifier: Int
public let title: String
public let contentBackgroundColor: UIColor
public let contentTextColor: UIColor
public let toolBackgroundColor: UIColor
public let toolControlTextColor: UIColor
public let toolLineColor: UIColor
public init(
identifier: Int,
title: String,
contentBackgroundColor: UIColor,
contentTextColor: UIColor,
toolBackgroundColor: UIColor,
toolControlTextColor: UIColor,
toolLineColor: UIColor
) {
self.identifier = identifier
self.title = title
self.contentBackgroundColor = contentBackgroundColor
self.contentTextColor = contentTextColor
self.toolBackgroundColor = toolBackgroundColor
self.toolControlTextColor = toolControlTextColor
self.toolLineColor = toolLineColor
}
}
public extension UIColor {
/// "#RRGGBB" `color` 使
convenience init(rdPDFHexString: String) {
let hex = rdPDFHexString.trimmingCharacters(in: .whitespacesAndNewlines).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
)
}
}