feat(reader): 增强阅读器功能与 UI 测试支持
- 新增字体选择(系统/宋体/圆体/等宽)与暗色图片柔化配置 - 文本选择改为自定义手势+操作栏(拷贝/高亮/批注) - 添加 accessibilityIdentifier 支持自动化 UI 测试 - 新增 UITests 覆盖阅读器打开/关闭、工具栏、设置面板、批注等 - 添加 Demo 测试用 EPUB 书源(宝山辽墓材料与释读) - 新增文档:UI 自动化测试、功能开发计划、阅读器规划
This commit is contained in:
@@ -9,6 +9,51 @@ public enum RDEPUBTextRenderingEngine: Equatable {
|
||||
case dtCoreText
|
||||
}
|
||||
|
||||
// MARK: - 阅读字体
|
||||
|
||||
/// 阅读器内置字体选项。
|
||||
/// 第一版优先使用系统授权字体,后续可扩展为 bundle 内置字体注册。
|
||||
public enum RDEPUBReaderFontChoice: String, Codable, CaseIterable, Equatable {
|
||||
/// 系统默认无衬线字体
|
||||
case system
|
||||
/// 系统衬线字体
|
||||
case serif
|
||||
/// 系统圆体字体
|
||||
case rounded
|
||||
/// 系统等宽字体
|
||||
case monospaced
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .system:
|
||||
return "系统"
|
||||
case .serif:
|
||||
return "宋体"
|
||||
case .rounded:
|
||||
return "圆体"
|
||||
case .monospaced:
|
||||
return "等宽"
|
||||
}
|
||||
}
|
||||
|
||||
public func font(ofSize size: CGFloat) -> UIFont {
|
||||
switch self {
|
||||
case .system:
|
||||
return UIFont.systemFont(ofSize: size)
|
||||
case .serif:
|
||||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||||
.withDesign(.serif) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||||
return UIFont(descriptor: descriptor, size: size)
|
||||
case .rounded:
|
||||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||||
.withDesign(.rounded) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||||
return UIFont(descriptor: descriptor, size: size)
|
||||
case .monospaced:
|
||||
return UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 阅读器配置
|
||||
|
||||
/// EPUB 阅读器的完整配置结构体
|
||||
@@ -21,6 +66,8 @@ public struct RDEPUBReaderConfiguration: Equatable {
|
||||
public var fontSize: CGFloat
|
||||
/// 行距倍数,默认 1.6(即行距为字号的 1.6 倍)
|
||||
public var lineHeightMultiple: CGFloat
|
||||
/// 正文字体,默认系统字体
|
||||
public var fontChoice: RDEPUBReaderFontChoice
|
||||
/// 栏数,默认单栏
|
||||
public var numberOfColumns: Int
|
||||
/// 栏间距,默认 20pt
|
||||
@@ -50,6 +97,10 @@ public struct RDEPUBReaderConfiguration: Equatable {
|
||||
|
||||
/// 当前阅读主题(浅色/深色/护眼等),默认 .light
|
||||
public var theme: RDEPUBReaderTheme
|
||||
/// 暗色主题下是否柔化正文图片,降低夜间阅读刺眼感
|
||||
public var darkImageAdjustmentEnabled: Bool
|
||||
/// 暗色主题图片柔化混合比例,0 表示不处理,推荐 0.12 ~ 0.2
|
||||
public var darkImageBlendRatio: CGFloat
|
||||
/// 固定布局的适配模式:按页适配或按宽度适配
|
||||
public var fixedLayoutFit: RDEPUBFixedLayoutFit
|
||||
/// 固定布局的跨页模式:自动/单页/双页
|
||||
@@ -63,6 +114,7 @@ public struct RDEPUBReaderConfiguration: Equatable {
|
||||
/// - Parameters:
|
||||
/// - fontSize: 正文字号,默认 15pt
|
||||
/// - lineHeightMultiple: 行距倍数,默认 1.6
|
||||
/// - fontChoice: 正文字体,默认系统字体
|
||||
/// - displayType: 翻页模式,默认 .pageCurl
|
||||
/// - landscapeDualPageEnabled: 横屏双页,默认 true
|
||||
/// - showsTableOfContents: 显示目录入口,默认 true
|
||||
@@ -71,12 +123,15 @@ public struct RDEPUBReaderConfiguration: Equatable {
|
||||
/// - reflowableContentInsets: 流式排版内边距
|
||||
/// - fixedContentInset: 固定布局内边距
|
||||
/// - theme: 阅读主题,默认 .light
|
||||
/// - darkImageAdjustmentEnabled: 暗色主题下是否柔化正文图片
|
||||
/// - darkImageBlendRatio: 暗色主题图片柔化混合比例
|
||||
/// - fixedLayoutFit: 固定布局适配模式
|
||||
/// - fixedLayoutSpreadMode: 固定布局跨页模式
|
||||
/// - textRenderingEngine: 文本渲染引擎
|
||||
public init(
|
||||
fontSize: CGFloat = 15,
|
||||
lineHeightMultiple: CGFloat = 1.6,
|
||||
fontChoice: RDEPUBReaderFontChoice = .system,
|
||||
numberOfColumns: Int = 1,
|
||||
columnGap: CGFloat = 20,
|
||||
displayType: RDReaderView.DisplayType = .pageCurl,
|
||||
@@ -87,12 +142,15 @@ public struct RDEPUBReaderConfiguration: Equatable {
|
||||
reflowableContentInsets: UIEdgeInsets = UIEdgeInsets(top: 40, left: 16, bottom: 40, right: 16),
|
||||
fixedContentInset: UIEdgeInsets = .zero,
|
||||
theme: RDEPUBReaderTheme = .light,
|
||||
darkImageAdjustmentEnabled: Bool = true,
|
||||
darkImageBlendRatio: CGFloat = 0.15,
|
||||
fixedLayoutFit: RDEPUBFixedLayoutFit = .page,
|
||||
fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode = .automatic,
|
||||
textRenderingEngine: RDEPUBTextRenderingEngine = .dtCoreText
|
||||
) {
|
||||
self.fontSize = fontSize
|
||||
self.lineHeightMultiple = lineHeightMultiple
|
||||
self.fontChoice = fontChoice
|
||||
self.numberOfColumns = max(1, numberOfColumns)
|
||||
self.columnGap = max(0, columnGap)
|
||||
self.displayType = displayType
|
||||
@@ -103,6 +161,8 @@ public struct RDEPUBReaderConfiguration: Equatable {
|
||||
self.reflowableContentInsets = reflowableContentInsets
|
||||
self.fixedContentInset = fixedContentInset
|
||||
self.theme = theme
|
||||
self.darkImageAdjustmentEnabled = darkImageAdjustmentEnabled
|
||||
self.darkImageBlendRatio = max(0, min(0.35, darkImageBlendRatio))
|
||||
self.fixedLayoutFit = fixedLayoutFit
|
||||
self.fixedLayoutSpreadMode = fixedLayoutSpreadMode
|
||||
self.textRenderingEngine = textRenderingEngine
|
||||
|
||||
@@ -110,6 +110,8 @@ public struct RDEPUBReaderSettings: Codable, Equatable {
|
||||
public var brightness: CGFloat?
|
||||
/// 用户选择的正文字号(pt),nil 表示使用默认值
|
||||
public var fontSize: CGFloat?
|
||||
/// 用户选择的正文字体,nil 表示使用默认值
|
||||
public var fontChoice: RDEPUBReaderFontChoice?
|
||||
/// 用户选择的行距倍数,nil 表示使用默认值
|
||||
public var lineHeightMultiple: CGFloat?
|
||||
/// 用户选择的栏数,nil 表示使用默认值
|
||||
@@ -123,6 +125,7 @@ public struct RDEPUBReaderSettings: Codable, Equatable {
|
||||
public init(
|
||||
brightness: CGFloat? = nil,
|
||||
fontSize: CGFloat? = nil,
|
||||
fontChoice: RDEPUBReaderFontChoice? = nil,
|
||||
lineHeightMultiple: CGFloat? = nil,
|
||||
numberOfColumns: Int? = nil,
|
||||
displayMode: RDEPUBReaderDisplayMode? = nil,
|
||||
@@ -130,6 +133,7 @@ public struct RDEPUBReaderSettings: Codable, Equatable {
|
||||
) {
|
||||
self.brightness = brightness
|
||||
self.fontSize = fontSize
|
||||
self.fontChoice = fontChoice
|
||||
self.lineHeightMultiple = lineHeightMultiple
|
||||
self.numberOfColumns = numberOfColumns
|
||||
self.displayMode = displayMode
|
||||
@@ -146,6 +150,9 @@ public struct RDEPUBReaderSettings: Codable, Equatable {
|
||||
if let fontSize {
|
||||
resolvedConfiguration.fontSize = fontSize
|
||||
}
|
||||
if let fontChoice {
|
||||
resolvedConfiguration.fontChoice = fontChoice
|
||||
}
|
||||
if let lineHeightMultiple {
|
||||
resolvedConfiguration.lineHeightMultiple = lineHeightMultiple
|
||||
}
|
||||
@@ -175,6 +182,7 @@ public struct RDEPUBReaderSettings: Codable, Equatable {
|
||||
RDEPUBReaderSettings(
|
||||
brightness: max(0, min(1, brightness)),
|
||||
fontSize: configuration.fontSize,
|
||||
fontChoice: configuration.fontChoice,
|
||||
lineHeightMultiple: configuration.lineHeightMultiple,
|
||||
numberOfColumns: configuration.numberOfColumns,
|
||||
displayMode: RDEPUBReaderDisplayMode(displayType: configuration.displayType),
|
||||
|
||||
@@ -12,6 +12,8 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
var onBrightnessChange: ((CGFloat) -> Void)?
|
||||
/// 字号变化回调(pt 值)
|
||||
var onFontSizeChange: ((CGFloat) -> Void)?
|
||||
/// 字体变化回调
|
||||
var onFontChoiceChange: ((RDEPUBReaderFontChoice) -> Void)?
|
||||
/// 行距倍数变化回调
|
||||
var onLineHeightChange: ((CGFloat) -> Void)?
|
||||
/// 栏数变化回调
|
||||
@@ -66,6 +68,7 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
private let fontValueLabel = UILabel()
|
||||
private let decreaseFontButton = UIButton(type: .system)
|
||||
private let increaseFontButton = UIButton(type: .system)
|
||||
private let fontChoiceControl = UISegmentedControl(items: RDEPUBReaderFontChoice.allCases.map(\.displayName))
|
||||
private let lineHeightControl = UISegmentedControl(items: ["紧凑", "标准", "宽松"])
|
||||
private let columnCountControl = UISegmentedControl(items: ["单栏", "双栏"])
|
||||
private let displayTypeControl = UISegmentedControl(items: ["仿真", "横滑", "竖滑"])
|
||||
@@ -103,11 +106,14 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
}
|
||||
|
||||
private func setupNavigationItems() {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "完成", style: .done, target: self, action: #selector(doneAction))
|
||||
let doneItem = UIBarButtonItem(title: "完成", style: .done, target: self, action: #selector(doneAction))
|
||||
doneItem.accessibilityIdentifier = "epub.reader.settings.done"
|
||||
navigationItem.rightBarButtonItem = doneItem
|
||||
}
|
||||
|
||||
private func setupViews() {
|
||||
view.addSubview(scrollView)
|
||||
scrollView.accessibilityIdentifier = "epub.reader.settings.scroll"
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.addSubview(contentStack)
|
||||
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -127,17 +133,26 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
|
||||
brightnessSlider.minimumValue = 0
|
||||
brightnessSlider.maximumValue = 1
|
||||
brightnessSlider.accessibilityIdentifier = "epub.reader.settings.brightness"
|
||||
brightnessSlider.addTarget(self, action: #selector(brightnessChanged(_:)), for: .valueChanged)
|
||||
|
||||
fontValueLabel.font = UIFont.monospacedDigitSystemFont(ofSize: 16, weight: .semibold)
|
||||
fontValueLabel.textAlignment = .center
|
||||
fontValueLabel.accessibilityIdentifier = "epub.reader.settings.font.value"
|
||||
fontValueLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
configureFontButton(decreaseFontButton, title: "A-")
|
||||
configureFontButton(increaseFontButton, title: "A+")
|
||||
decreaseFontButton.accessibilityIdentifier = "epub.reader.settings.font.decrease"
|
||||
increaseFontButton.accessibilityIdentifier = "epub.reader.settings.font.increase"
|
||||
decreaseFontButton.addTarget(self, action: #selector(decreaseFontAction), for: .touchUpInside)
|
||||
increaseFontButton.addTarget(self, action: #selector(increaseFontAction), for: .touchUpInside)
|
||||
|
||||
lineHeightControl.accessibilityIdentifier = "epub.reader.settings.lineHeight"
|
||||
fontChoiceControl.accessibilityIdentifier = "epub.reader.settings.font.choice"
|
||||
columnCountControl.accessibilityIdentifier = "epub.reader.settings.columns"
|
||||
displayTypeControl.accessibilityIdentifier = "epub.reader.settings.displayType"
|
||||
fontChoiceControl.addTarget(self, action: #selector(fontChoiceChanged(_:)), for: .valueChanged)
|
||||
lineHeightControl.addTarget(self, action: #selector(lineHeightChanged(_:)), for: .valueChanged)
|
||||
columnCountControl.addTarget(self, action: #selector(columnCountChanged(_:)), for: .valueChanged)
|
||||
displayTypeControl.addTarget(self, action: #selector(displayTypeChanged(_:)), for: .valueChanged)
|
||||
@@ -149,6 +164,7 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
button.layer.borderWidth = 1.5
|
||||
button.backgroundColor = preset.theme.contentBackgroundColor
|
||||
button.accessibilityLabel = preset.title
|
||||
button.accessibilityIdentifier = "epub.reader.settings.theme.\(preset.rawValue)"
|
||||
button.addTarget(self, action: #selector(themeButtonAction(_:)), for: .touchUpInside)
|
||||
themeButtons.append(button)
|
||||
themeStackView.addArrangedSubview(button)
|
||||
@@ -159,6 +175,7 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
|
||||
contentStack.addArrangedSubview(makeSection(title: "亮度", content: brightnessSlider))
|
||||
contentStack.addArrangedSubview(makeSection(title: "字号", content: makeFontSizeRow()))
|
||||
contentStack.addArrangedSubview(makeSection(title: "字体", content: fontChoiceControl))
|
||||
contentStack.addArrangedSubview(makeSection(title: "行距", content: lineHeightControl))
|
||||
contentStack.addArrangedSubview(makeSection(title: "分栏", content: columnCountControl))
|
||||
contentStack.addArrangedSubview(makeSection(title: "翻页方式", content: displayTypeControl))
|
||||
@@ -205,6 +222,7 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
|
||||
private func syncControls() {
|
||||
fontValueLabel.text = String(Int(currentConfiguration.fontSize.rounded()))
|
||||
fontChoiceControl.selectedSegmentIndex = RDEPUBReaderFontChoice.allCases.firstIndex(of: currentConfiguration.fontChoice) ?? 0
|
||||
|
||||
let lineHeightIndex = lineHeightValues.enumerated().min { abs($0.element - currentConfiguration.lineHeightMultiple) < abs($1.element - currentConfiguration.lineHeightMultiple) }?.offset ?? 1
|
||||
lineHeightControl.selectedSegmentIndex = lineHeightIndex
|
||||
@@ -239,7 +257,7 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
button.backgroundColor = theme.toolBackgroundColor
|
||||
}
|
||||
|
||||
[lineHeightControl, columnCountControl, displayTypeControl].forEach { control in
|
||||
[fontChoiceControl, lineHeightControl, columnCountControl, displayTypeControl].forEach { control in
|
||||
control.backgroundColor = theme.toolBackgroundColor
|
||||
if #available(iOS 13.0, *) {
|
||||
control.selectedSegmentTintColor = theme.toolControlTextColor.withAlphaComponent(0.14)
|
||||
@@ -288,6 +306,15 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
onFontSizeChange?(nextValue)
|
||||
}
|
||||
|
||||
@objc private func fontChoiceChanged(_ control: UISegmentedControl) {
|
||||
let choices = RDEPUBReaderFontChoice.allCases
|
||||
let index = max(0, min(control.selectedSegmentIndex, choices.count - 1))
|
||||
let choice = choices[index]
|
||||
guard choice != currentConfiguration.fontChoice else { return }
|
||||
currentConfiguration.fontChoice = choice
|
||||
onFontChoiceChange?(choice)
|
||||
}
|
||||
|
||||
@objc private func lineHeightChanged(_ control: UISegmentedControl) {
|
||||
let index = max(0, min(control.selectedSegmentIndex, lineHeightValues.count - 1))
|
||||
let value = lineHeightValues[index]
|
||||
|
||||
Reference in New Issue
Block a user