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>
252 lines
11 KiB
Swift
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)
|
|
}
|
|
}
|