将 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:
parent
1bde945d36
commit
a17164d1a9
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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?() }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
46
Sources/RDPDFReaderView/Sources/RDPDFReaderTheme.swift
Normal file
46
Sources/RDPDFReaderView/Sources/RDPDFReaderTheme.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user