Files
ReadViewSDK/Sources/RDPDFReaderView/Sources/RDPDFReaderSettingsPanelViewController.swift
T
shenleiandClaude Fable 5 1422461224 将 PDF 成品阅读控制器下沉为 RDPDFReaderView SDK,并修复手机横屏单页连续竖滑
SDK 化收尾:
- 新增 RDPDFReaderView 本地 pod(阅读容器、成品控制器、划线/注释、画笔、
  目录/设置面板、主题与持久化契约),Demo 精简为 PDFKit 页面提供者 + 持久化宿主
- Podfile/工程接入 RDPDFReaderView 与 SnapKit,示例书改用 PDF 阅读器示例文件

手机横屏单页竖滑修复:
- cell 高度改为宽度适配后的纸张高度(sizeForItemAt + 宿主按页提供纵横比),
  纵向超出一屏的内容由外层 collectionView 连续滚动承接,不再被裁掉
- 页面内部不再开启同向嵌套的兜底竖向拖动,修复外层滚动被吞、无法滚到下一页
- 修复 applyContentSize 先评估拖动开关后写 contentSize 的顺序缺陷
- 页码按视口中心命中 cell 计算,补 didEndDragging 兜底;旋转重锚点与
  transitionToPage 改用真实布局位置
- 横竖屏同为竖滑时也重建 cell 并刷新全部可见页的适配配置
- 横屏单页点击不再跳页,只显隐工具栏;翻页交给连续竖滑

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 18:49:44 +09:00

252 lines
11 KiB
Swift

import UIKit
import SnapKit
/// 阅读设置面板:亮度、翻页方式、主题。
/// 主题列表由宿主传入 `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
private let allowsDisplayTypeSelection: Bool
public init(
displayType: RDPDFReaderView.DisplayType,
brightness: CGFloat,
themes: [RDPDFReaderThemeOption],
selectedThemeIdentifier: Int,
allowsDisplayTypeSelection: Bool = true
) {
precondition(!themes.isEmpty, "至少需要一个主题选项")
self.themes = themes
currentDisplayType = displayType
currentTheme = themes.first { $0.identifier == selectedThemeIdentifier } ?? themes[0]
self.allowsDisplayTypeSelection = allowsDisplayTypeSelection
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(view.addSubview)
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)
displayTypeControl.isEnabled = allowsDisplayTypeSelection
displayTypeControl.accessibilityHint = allowsDisplayTypeSelection ? nil : "手机横屏时固定为上下滚动"
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.snp.makeConstraints { $0.height.equalTo(30) }
}
let brightnessLabel = makeLabel("亮度")
let displayTypeLabel = makeLabel(allowsDisplayTypeSelection ? "翻页方式" : "翻页方式(横屏已固定为竖滑)")
let themeLabel = makeLabel("主题")
let dividerOne = makeDivider()
let dividerTwo = makeDivider()
[brightnessLabel, displayTypeLabel, themeLabel, brightnessSlider, displayTypeControl, themeStack, dividerOne, dividerTwo].forEach(contentView.addSubview)
handleView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.centerX.equalToSuperview()
make.size.equalTo(CGSize(width: 42, height: 5))
}
closeButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(16)
make.centerY.equalTo(handleView.snp.centerY).offset(20)
make.size.equalTo(32)
}
scrollView.snp.makeConstraints { make in
make.top.equalTo(handleView.snp.bottom).offset(14)
make.horizontalEdges.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide)
}
contentView.snp.makeConstraints { make in
make.edges.equalTo(scrollView.contentLayoutGuide)
make.width.equalTo(scrollView.frameLayoutGuide)
}
brightnessLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.top.equalToSuperview().offset(8)
}
brightnessSlider.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview().inset(20)
make.top.equalTo(brightnessLabel.snp.bottom).offset(8)
}
dividerOne.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview().inset(20)
make.top.equalTo(brightnessSlider.snp.bottom).offset(14)
}
displayTypeLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.top.equalTo(dividerOne.snp.bottom).offset(14)
}
displayTypeControl.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview().inset(20)
make.top.equalTo(displayTypeLabel.snp.bottom).offset(8)
make.height.equalTo(32)
}
dividerTwo.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview().inset(20)
make.top.equalTo(displayTypeControl.snp.bottom).offset(14)
}
themeLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.top.equalTo(dividerTwo.snp.bottom).offset(14)
}
themeStack.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview().inset(24)
make.top.equalTo(themeLabel.snp.bottom).offset(12)
make.bottom.equalToSuperview().inset(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.snp.makeConstraints { $0.height.equalTo(1 / UIScreen.main.scale) }
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)
}
}