- 新增 RDEPUBReaderSearchCoordinator 与 RDEPUBSelectionState 管理搜索和选中状态 - 新增 BookmarkChromeStateTests、NavigationBackwardTests、SelectionAnnotateTests 等 UI 测试 - 新增多个边界测试 epub 样本(损坏结构、空归档、缺失文件、流式外链验证) - 重构阅读器 chrome 状态管理,统一 tool bar 与 search bar 交互 - 优化大书分页缓存策略(RDEPUBChapterSummaryDiskCache、RDEPUBPageCountCache) - 移除废弃的 RDEPUBLocationConverter 和 RDEPUBPageBreakPolicy - 更新 epub-bridge.js 与 JS bridge 通信协议 - 全面更新现有 UI 测试以适配新的 helper 和状态管理
607 lines
22 KiB
Markdown
607 lines
22 KiB
Markdown
# Android EPUB 阅读器实施方案
|
||
|
||
## Context
|
||
|
||
当前 ReadViewSDK 是一个功能完整的 iOS EPUB 阅读器框架(Swift + UIKit),支持 EPUB 2/3 解析、三种渲染模式(重排/固定布局/网页交互)、全文搜索、高亮/书签/批注、分页管理等。目标是基于现有 iOS 版本的架构和 JS 桥接协议,实现一套 Android 版本的 EPUB 阅读器。
|
||
|
||
**核心策略**:JS 桥接层(epub-bridge.js、WeReadApi.js 等)直接复用,Swift 逻辑层用 Kotlin 重写,UIKit UI 层用 Android 原生 View 重写。
|
||
|
||
---
|
||
|
||
## 第一阶段:项目基础设施(预计 1 周)
|
||
|
||
### 1.1 创建 Android 项目
|
||
|
||
```
|
||
ReadViewSDK-Android/
|
||
├── app/ # Demo 应用
|
||
├── reader-core/ # Kotlin 核心库(解析、模型、搜索)
|
||
├── reader-jsbridge/ # WebView JS 桥接层
|
||
├── reader-ui/ # Android UI 组件(工具栏、搜索面板、设置等)
|
||
├── reader-view/ # 翻页容器(ViewPager2 封装)
|
||
└── build-logic/ # Gradle convention plugins
|
||
```
|
||
|
||
- 语言:Kotlin
|
||
- 最低 API:24(Android 7.0)
|
||
- 目标 API:34
|
||
- 构建工具:Gradle + Kotlin DSL
|
||
- 依赖注入:手动(与 iOS 的 `RDEPUBReaderDependencies` 模式一致)
|
||
|
||
### 1.2 Gradle 模块划分
|
||
|
||
| 模块 | 对应 iOS 层 | 职责 |
|
||
|------|------------|------|
|
||
| `reader-core` | EPUBCore/ + EPUBTextRendering/ | 解析、模型、搜索引擎、CSS 生成、分页算法 |
|
||
| `reader-jsbridge` | RDEPUBJavaScriptBridge + JS Resources | JS 文件管理、消息收发、脚本生成 |
|
||
| `reader-ui` | EPUBUI/ | 工具栏、搜索面板、设置面板、目录列表、高亮管理 |
|
||
| `reader-view` | ReaderView/ | 翻页容器(ViewPager2 + PagerSnapHelper) |
|
||
|
||
### 1.3 公共依赖
|
||
|
||
```kotlin
|
||
// reader-core
|
||
implementation("net.lingala.zip4j:zip4j:2.11.5") // 替代 ZIPFoundation
|
||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
|
||
implementation("com.google.code.gson:gson:2.11.0") // 替代 Codable
|
||
implementation("androidx.webkit:webkit:1.10.0")
|
||
|
||
// reader-ui
|
||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||
implementation("com.google.android.material:material:1.12.0")
|
||
implementation("androidx.viewpager2:viewpager2:1.0.0")
|
||
|
||
// reader-view
|
||
implementation("androidx.viewpager2:viewpager2:1.0.0")
|
||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||
```
|
||
|
||
---
|
||
|
||
## 第二阶段:Core 层 — EPUB 解析与模型(预计 2 周)
|
||
|
||
### 2.1 数据模型迁移
|
||
|
||
将 iOS 的 Codable struct 迁移为 Kotlin data class:
|
||
|
||
| iOS 文件 | Android 文件 | 关键类型 |
|
||
|----------|-------------|---------|
|
||
| `RDEPUBModels.swift` | `EpubModels.kt` | `EpubLayout`, `EpubMetadata`, `ManifestItem`, `SpineItem`, `TableOfContentsItem` |
|
||
| `RDEPUBReadingLocationModels.swift` | `ReadingLocationModels.kt` | `ReadingLocation`, `Viewport`, `ReadingContext` |
|
||
| `RDEPUBAnnotationModels.swift` | `AnnotationModels.kt` | `Selection`, `Highlight`, `Bookmark`, `Annotation` |
|
||
| `RDEPUBPaginationModels.swift` | `PaginationModels.kt` | `EpubPage`, `ChapterInfo`, `FixedSpread` |
|
||
| `RDEPUBSearchModels.swift` | `SearchModels.kt` | `SearchMatch`, `SearchResult`, `SearchState`, `SearchPresentation` |
|
||
| `RDEPUBPreferences.swift` | `ReaderPreferences.kt` | `ReadingPreferences`(字体、行高、颜色、分栏) |
|
||
| `RDEPUBRenderRequest.swift` | `RenderRequest.kt` | `ReflowableRenderRequest`, `FixedRenderRequest` |
|
||
| `RDEPUBReaderConfiguration.swift` | `ReaderConfiguration.kt` | 完整阅读器配置 |
|
||
| `RDEPUBReaderTheme.swift` | `ReaderTheme.kt` | 6 种预设主题 |
|
||
|
||
### 2.2 EPUB 解析器迁移
|
||
|
||
| iOS 文件 | Android 文件 | 说明 |
|
||
|----------|-------------|------|
|
||
| `RDEPUBParser.swift` | `EpubParser.kt` | 主解析入口 |
|
||
| `RDEPUBParser+Archive.swift` | `EpubParser+Archive.kt` | zip4j 解压(替代 ZIPFoundation) |
|
||
| `RDEPUBParser+Package.swift` | `EpubParser+Package.kt` | OPF XML SAX 解析 |
|
||
| `RDEPUBParser+TOC.swift` | `EpubParser+TOC.kt` | NCX/Nav Document 解析 |
|
||
| `RDEPUBParser+Resources.swift` | `EpubParser+Resources.kt` | HTML/资源文件读取 |
|
||
| `RDEPUBParser+ReadingProfile.swift` | `EpubParser+ReadingProfile.kt` | 阅读模式判断 |
|
||
| `RDEPUBPublication.swift` | `EpubPublication.kt` | Facade 门面 |
|
||
| `RDEPUBResourceResolver.swift` | `ResourceResolver.kt` | URL 规范化 |
|
||
|
||
**XML 解析方案**:iOS 使用 `XMLParser`(SAX),Android 使用 `org.xml.sax.XMLReader`(同为 SAX),解析逻辑可逐行对照翻译。
|
||
|
||
### 2.3 搜索引擎迁移
|
||
|
||
| iOS 文件 | Android 文件 |
|
||
|----------|-------------|
|
||
| `RDEPUBSearchEngine.swift` | `EpubSearchEngine.kt` |
|
||
| `RDEPUBTextSearchEngine.swift` | `TextSearchEngine.kt` |
|
||
|
||
搜索引擎核心逻辑(遍历 spine → 读取 HTML → 解析为纯文本 → NSString.range 搜索 → 生成 match)可直接翻译为 Kotlin `String.indexOf` 循环。
|
||
|
||
### 2.4 CSS 生成与样式
|
||
|
||
| iOS 文件 | Android 文件 |
|
||
|----------|-------------|
|
||
| `RDEPUBStyleSheetBuilder.swift` | `StyleSheetBuilder.kt` |
|
||
| `RDEPUBFixedLayoutTemplate.swift` | `FixedLayoutTemplate.kt` |
|
||
|
||
CSS 生成逻辑完全平台无关,直接翻译即可。
|
||
|
||
### 2.5 分页算法
|
||
|
||
| iOS 文件 | Android 文件 | 说明 |
|
||
|----------|-------------|------|
|
||
| `RDEPUBPaginator.swift` | `EpubPaginator.kt` | 离屏 WebView 分页计算 |
|
||
| `RDEPUBReadingSession.swift` | `ReadingSession.kt` | 状态机 + 页面管理 |
|
||
|
||
**关键决策**:Android 版统一使用 WebView 渲染,不实现 `EPUBTextRendering` 层(DTCoreText 的 Android 等价物过于复杂且收益低)。这意味着:
|
||
|
||
- **不迁移** `EPUBTextRendering/` 整个目录(18 个文件)
|
||
- **不迁移** `EPUBUI/TextPage/` 目录(7 个文件)
|
||
- Android 版的所有内容(包括重排模式)都通过 WebView + CSS column 分页实现
|
||
- 这与 iOS 的 `webInteractive` 路径一致,已验证可行
|
||
|
||
---
|
||
|
||
## 第三阶段:JS 桥接层(预计 1 周)
|
||
|
||
### 3.1 JS 文件直接复用
|
||
|
||
将以下文件直接复制到 Android `assets/` 目录:
|
||
|
||
```
|
||
reader-jsbridge/src/main/assets/js/
|
||
├── epub-bridge.js # 直接复用
|
||
├── WeReadApi.js # 直接复用
|
||
├── cssInjector.js # 直接复用
|
||
├── rangy-core.js # 直接复用
|
||
└── rangy-serializer.js # 直接复用
|
||
```
|
||
|
||
### 3.2 WKWebView → Android WebView 桥接映射
|
||
|
||
| iOS 机制 | Android 等价物 |
|
||
|----------|--------------|
|
||
| `WKScriptMessageHandler.userContentController(_:didReceive:)` | `@JavascriptInterface` 方法 |
|
||
| `window.webkit.messageHandlers.{name}.postMessage()` | 修改 JS 端调用 `AndroidBridge.{method}()` |
|
||
| `WKWebView.evaluateJavaScript()` | `WebView.evaluateJavascript()` |
|
||
| `WKURLSchemeHandler` | `WebViewClient.shouldInterceptRequest()` |
|
||
| `WKUserScript`(页面加载前注入) | `WebView.addJavascriptInterface()` + `WebViewClient.onPageStarted()` |
|
||
|
||
### 3.3 Android Bridge 实现
|
||
|
||
```kotlin
|
||
// EpubBridge.kt — 对应 iOS RDEPUBJavaScriptBridge
|
||
class EpubBridge(private val callback: BridgeCallback) {
|
||
|
||
@JavascriptInterface
|
||
fun onProgressionChanged(json: String) { callback.onProgressionChanged(json) }
|
||
|
||
@JavascriptInterface
|
||
fun onSelectionChanged(json: String) { callback.onSelectionChanged(json) }
|
||
|
||
@JavascriptInterface
|
||
fun onInternalLink(href: String) { callback.onInternalLink(href) }
|
||
|
||
@JavascriptInterface
|
||
fun onExternalLink(url: String) { callback.onExternalLink(url) }
|
||
|
||
@JavascriptInterface
|
||
fun onJSError(message: String) { callback.onJSError(message) }
|
||
|
||
@JavascriptInterface
|
||
fun onFixedLayoutReady() { callback.onFixedLayoutReady() }
|
||
}
|
||
```
|
||
|
||
**JS 端修改**:需要将 `window.webkit.messageHandlers.xxx.postMessage(data)` 替换为 `window.AndroidBridge.xxx(JSON.stringify(data))`。可以通过在注入时做字符串替换,或维护一个 Android 版本的 bridge JS 文件。
|
||
|
||
### 3.4 WebView 资源拦截
|
||
|
||
对应 iOS 的 `ss-reader://book/` 自定义协议:
|
||
|
||
```kotlin
|
||
// EpubSchemeHandler.kt
|
||
class EpubSchemeHandler(private val publication: EpubPublication) : WebViewClient() {
|
||
|
||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||
val url = request.url.toString()
|
||
if (url.startsWith("ss-reader://book/")) {
|
||
val path = url.removePrefix("ss-reader://book/")
|
||
val data = publication.readResource(path)
|
||
val mimeType = getMimeType(path)
|
||
return WebResourceResponse(mimeType, "UTF-8", data.byteInputStream())
|
||
}
|
||
return null
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.5 JavaScriptBridge 脚本生成
|
||
|
||
对应 iOS 的 `RDEPUBJavaScriptBridge` 中各种 `*Script()` 方法,生成 `evaluateJavascript()` 调用的 JS 代码字符串。这些方法的核心是拼接 JSON 参数 + 调用 `WeReadApi.*` 方法,逻辑完全相同。
|
||
|
||
---
|
||
|
||
## 第四阶段:翻页容器(预计 1.5 周)
|
||
|
||
### 4.1 PagerView 实现
|
||
|
||
对应 iOS `RDReaderView`(UICollectionView + UIPageViewController):
|
||
|
||
```kotlin
|
||
// PagerView.kt — 对应 iOS RDReaderView
|
||
class PagerView @JvmOverloads constructor(
|
||
context: Context, attrs: AttributeSet? = null
|
||
) : FrameLayout(context, attrs) {
|
||
|
||
private val viewPager: ViewPager2
|
||
private val adapter: PageAdapter
|
||
|
||
enum class DisplayMode { HORIZONTAL, VERTICAL }
|
||
var displayMode = DisplayMode.HORIZONTAL
|
||
set(value) { /* 切换 orientation */ }
|
||
|
||
fun reloadData() { adapter.notifyDataSetChanged() }
|
||
fun scrollToPage(index: Int, animated: Boolean) { /* ... */ }
|
||
}
|
||
```
|
||
|
||
**PageAdapter**:使用 `RecyclerView.Adapter`,每个 item 是一个 `FrameLayout`,内部放 `WebView`。
|
||
|
||
### 4.2 点击区域检测
|
||
|
||
对应 iOS `RDReaderTapRegionHandler`:
|
||
|
||
```kotlin
|
||
// TapRegionHandler.kt
|
||
class TapRegionHandler {
|
||
enum class Region { LEFT, CENTER, RIGHT }
|
||
|
||
fun resolve(x: Float, viewWidth: Float): Region {
|
||
return when {
|
||
x < viewWidth / 3 -> Region.LEFT
|
||
x > viewWidth * 2 / 3 -> Region.RIGHT
|
||
else -> Region.CENTER
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.3 预加载控制
|
||
|
||
对应 iOS `RDReaderPreloadController`:
|
||
|
||
使用 `ViewPager2.offscreenPageLimit` 控制预加载页面数量(默认 1-2 页)。
|
||
|
||
---
|
||
|
||
## 第五阶段:UI 组件(预计 2 周)
|
||
|
||
### 5.1 阅读器主控制器
|
||
|
||
对应 iOS `RDEPUBReaderController`:
|
||
|
||
```kotlin
|
||
// EpubReaderFragment.kt — Fragment 优于 Activity,方便宿主嵌入
|
||
class EpubReaderFragment : Fragment() {
|
||
|
||
// 对应 iOS 的各 Coordinator
|
||
private lateinit var runtime: ReaderRuntime
|
||
private lateinit var pagerView: PagerView
|
||
private lateinit var topBar: TopToolBar
|
||
private lateinit var bottomBar: BottomToolBar
|
||
private lateinit var searchPanel: SearchPanelView
|
||
|
||
// Public API
|
||
fun openBook(epubUrl: Uri, configuration: ReaderConfiguration = ReaderConfiguration.default)
|
||
fun goTo(location: ReadingLocation)
|
||
fun goToPageNumber(pageNumber: Int)
|
||
fun search(keyword: String)
|
||
fun addHighlight(selection: Selection, color: Int, note: String?)
|
||
fun addBookmark(note: String?)
|
||
// ... 其余 public API
|
||
}
|
||
```
|
||
|
||
### 5.2 Coordinator 架构迁移
|
||
|
||
保持 iOS 的 Coordinator 模式,每个 Coordinator 作为独立的 Kotlin 类:
|
||
|
||
| iOS Coordinator | Android 类 | 职责 |
|
||
|----------------|-----------|------|
|
||
| `RDEPUBReaderRuntime` | `ReaderRuntime` | Facade,协调所有子 Coordinator |
|
||
| `RDEPUBReaderLoadCoordinator` | `LoadCoordinator` | EPUB 文件解析 + Publication 初始化 |
|
||
| `RDEPUBReaderPaginationCoordinator` | `PaginationCoordinator` | 分页计算 |
|
||
| `RDEPUBReaderLocationCoordinator` | `LocationCoordinator` | 阅读位置管理 + 持久化 |
|
||
| `RDEPUBReaderChromeCoordinator` | `ChromeCoordinator` | 工具栏状态同步 |
|
||
| `RDEPUBReaderAnnotationCoordinator` | `AnnotationCoordinator` | 高亮/书签/批注 CRUD |
|
||
| `RDEPUBReaderSearchCoordinator` | `SearchCoordinator` | 全文搜索协调 |
|
||
| `RDEPUBReaderViewportMonitor` | `ViewportMonitor` | 屏幕旋转/尺寸变化检测 |
|
||
|
||
### 5.3 WebView 内容视图
|
||
|
||
对应 iOS `RDEPUBWebView` + `RDEPUBWebContentView`:
|
||
|
||
```kotlin
|
||
// EpubWebView.kt — 封装 Android WebView
|
||
class EpubWebView @JvmOverloads constructor(
|
||
context: Context, attrs: AttributeSet? = null
|
||
) : WebView(context, attrs) {
|
||
|
||
private val bridge = EpubBridge(/* ... */)
|
||
|
||
fun setup() {
|
||
settings.javaScriptEnabled = true
|
||
settings.domStorageEnabled = true
|
||
addJavascriptInterface(bridge, "AndroidBridge")
|
||
webViewClient = EpubSchemeHandler(publication)
|
||
webChromeClient = WebChromeClient()
|
||
}
|
||
|
||
fun loadChapter(spineIndex: Int, preferences: ReadingPreferences) {
|
||
// 生成 HTML 模板 + 注入 CSS + 加载资源
|
||
}
|
||
|
||
fun evaluateScript(script: String, callback: ((String?) -> Unit)? = null) {
|
||
evaluateJavascript(script) { result -> callback?.invoke(result) }
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.4 顶部工具栏
|
||
|
||
对应 iOS `RDEPUBReaderTopToolView`:
|
||
|
||
```xml
|
||
<!-- top_tool_bar.xml -->
|
||
<LinearLayout>
|
||
<ImageButton android:id="@+id/btnBack" />
|
||
<TextView android:id="@+id/tvTitle" weight="1" />
|
||
<ImageButton android:id="@+id/btnSearch" />
|
||
<ImageButton android:id="@+id/btnBookmark" />
|
||
</LinearLayout>
|
||
```
|
||
|
||
### 5.5 底部工具栏
|
||
|
||
对应 iOS `RDEPUBReaderBottomToolView`:
|
||
|
||
```xml
|
||
<!-- bottom_tool_bar.xml -->
|
||
<LinearLayout>
|
||
<ImageButton android:id="@+id/btnTOC" /> <!-- 目录 -->
|
||
<ImageButton android:id="@+id/btnBookmarks" /> <!-- 书签列表 -->
|
||
<ImageButton android:id="@+id/btnHighlights" /> <!-- 高亮列表 -->
|
||
<ImageButton android:id="@+id/btnAddHighlight" /> <!-- 添加高亮 -->
|
||
<ImageButton android:id="@+id/btnSettings" /> <!-- 设置 -->
|
||
</LinearLayout>
|
||
```
|
||
|
||
### 5.6 搜索面板
|
||
|
||
对应 iOS `RDEPUBReaderSearchBarView`:
|
||
|
||
```kotlin
|
||
// SearchPanelView.kt
|
||
class SearchPanelView @JvmOverloads constructor(
|
||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||
|
||
private val searchField: EditText
|
||
private val cancelButton: Button
|
||
private val resultsList: RecyclerView // 分组结果列表
|
||
private val emptyStateLabel: TextView
|
||
|
||
fun updateResults(sections: List<SearchSection>, keyword: String, currentMatchIndex: Int?)
|
||
fun showNoResults()
|
||
fun showSearching()
|
||
}
|
||
```
|
||
|
||
### 5.7 设置面板
|
||
|
||
对应 iOS `RDEPUBReaderSettingsViewController`:
|
||
|
||
使用 `BottomSheetDialogFragment` 实现底部弹出设置面板:
|
||
- 亮度调节(SeekBar)
|
||
- 字体大小(+/- 按钮)
|
||
- 字体选择
|
||
- 行间距
|
||
- 主题切换(6 种预设)
|
||
- 翻页模式切换
|
||
|
||
### 5.8 目录列表
|
||
|
||
对应 iOS `RDEPUBReaderChapterListController`:
|
||
|
||
使用 `BottomSheetDialogFragment` + `RecyclerView` 实现目录列表,支持章节标题显示和点击跳转。
|
||
|
||
---
|
||
|
||
## 第六阶段:持久化与状态管理(预计 0.5 周)
|
||
|
||
### 6.1 持久化实现
|
||
|
||
对应 iOS `RDEPUBReaderPersistence`:
|
||
|
||
```kotlin
|
||
// ReaderPersistence.kt
|
||
interface ReaderPersistence {
|
||
fun loadLocation(bookId: String): ReadingLocation?
|
||
fun saveLocation(bookId: String, location: ReadingLocation)
|
||
fun loadBookmarks(bookId: String): List<Bookmark>
|
||
fun saveBookmarks(bookId: String, bookmarks: List<Bookmark>)
|
||
fun loadHighlights(bookId: String): List<Highlight>
|
||
fun saveHighlights(bookId: String, highlights: List<Highlight>)
|
||
fun loadSettings(): ReaderSettings
|
||
fun saveSettings(settings: ReaderSettings)
|
||
}
|
||
|
||
// SharedPreferences 实现
|
||
class SharedPrefsPersistence(context: Context) : ReaderPersistence {
|
||
// 使用 Gson 序列化/反序列化
|
||
}
|
||
```
|
||
|
||
### 6.2 状态管理
|
||
|
||
使用 Kotlin `StateFlow` 替代 iOS 的手动状态同步:
|
||
|
||
```kotlin
|
||
// ReaderState.kt
|
||
data class ReaderUiState(
|
||
val isLoading: Boolean = true,
|
||
val currentPage: Int = 0,
|
||
val totalPages: Int = 0,
|
||
val title: String = "",
|
||
val isToolbarVisible: Boolean = false,
|
||
val isSearchVisible: Boolean = false,
|
||
val bookmarks: List<Bookmark> = emptyList(),
|
||
val highlights: List<Highlight> = emptyList(),
|
||
val searchState: SearchState? = null,
|
||
)
|
||
|
||
class ReaderStateManager {
|
||
private val _uiState = MutableStateFlow(ReaderUiState())
|
||
val uiState: StateFlow<ReaderUiState> = _uiState.asStateFlow()
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 第七阶段:主题与外观(预计 0.5 周)
|
||
|
||
### 7.1 主题系统
|
||
|
||
对应 iOS `RDEPUBReaderTheme`:
|
||
|
||
```kotlin
|
||
// ReaderTheme.kt
|
||
data class ReaderTheme(
|
||
val name: String,
|
||
val contentBackgroundColor: Int, // Color int
|
||
val textColor: Int,
|
||
val toolbarBackgroundColor: Int,
|
||
val toolbarForegroundColor: Int,
|
||
val isDark: Boolean
|
||
) {
|
||
companion object {
|
||
val LIGHT = ReaderTheme("浅色", Color.WHITE, Color.BLACK, ...)
|
||
val DARK = ReaderTheme("深色", Color.rgb(0x1A, 0x1A, 0x1A), Color.WHITE, ...)
|
||
val YELLOW = ReaderTheme("纸质", Color.rgb(0xF5, 0xF0, 0xE0), Color.BLACK, ...)
|
||
// ... 共 6 种
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.2 状态栏适配
|
||
|
||
根据主题动态设置状态栏颜色(`Window.statusBarColor`)和图标颜色(`WindowInsetsController.isAppearanceLightStatusBars`)。
|
||
|
||
---
|
||
|
||
## 第八阶段:测试与 Demo(预计 1 周)
|
||
|
||
### 8.1 单元测试
|
||
|
||
- `EpubParserTest` — EPUB 解析(container.xml、OPF、TOC)
|
||
- `SearchEngineTest` — 搜索引擎
|
||
- `StyleSheetBuilderTest` — CSS 生成
|
||
- `ResourceResolverTest` — URL 解析
|
||
- `LocationCoordinatorTest` — 位置管理
|
||
|
||
### 8.2 集成测试
|
||
|
||
- `EpubReaderIntegrationTest` — 完整阅读流程(打开 → 分页 → 翻页 → 搜索 → 高亮)
|
||
|
||
### 8.3 Demo App
|
||
|
||
对应 iOS `ReadViewDemo`:
|
||
|
||
```kotlin
|
||
// MainActivity.kt
|
||
class MainActivity : AppCompatActivity() {
|
||
private fun openBook() {
|
||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||
addCategory(Intent.CATEGORY_OPENABLE)
|
||
type = "application/epub+zip"
|
||
}
|
||
startActivityForResult(intent, REQUEST_OPEN_BOOK)
|
||
}
|
||
|
||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||
if (requestCode == REQUEST_OPEN_BOOK && resultCode == RESULT_OK) {
|
||
val uri = data?.data ?: return
|
||
val fragment = EpubReaderFragment.newInstance(uri)
|
||
// 嵌入 Fragment
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.4 UI 自动化测试
|
||
|
||
对应 iOS 的 XCUIApplication 测试(24 个测试文件):
|
||
- 使用 Espresso + UiAutomator
|
||
- 覆盖:目录交互、页码导航、书签管理、高亮管理、搜索、设置效果、工具栏状态等
|
||
|
||
---
|
||
|
||
## 实施时间估算
|
||
|
||
| 阶段 | 内容 | 预计工时 |
|
||
|------|------|---------|
|
||
| 1 | 项目基础设施 | 1 周 |
|
||
| 2 | Core 层(解析、模型、搜索、CSS) | 2 周 |
|
||
| 3 | JS 桥接层 | 1 周 |
|
||
| 4 | 翻页容器 | 1.5 周 |
|
||
| 5 | UI 组件 | 2 周 |
|
||
| 6 | 持久化与状态管理 | 0.5 周 |
|
||
| 7 | 主题与外观 | 0.5 周 |
|
||
| 8 | 测试与 Demo | 1 周 |
|
||
| **合计** | | **约 9.5 周** |
|
||
|
||
---
|
||
|
||
## 关键技术决策
|
||
|
||
1. **统一 WebView 渲染**:不实现 DTCoreText 等效层,所有内容通过 WebView + CSS column 分页。简化实现,与 iOS 的 webInteractive 路径一致。
|
||
|
||
2. **Coordinator 模式保留**:与 iOS 架构保持一致,便于两端逻辑对照和维护。
|
||
|
||
3. **JS Bridge 微调**:Android 的 `@JavascriptInterface` 与 iOS 的 `WKScriptMessageHandler` 机制不同,JS 端需要适配(用 `AndroidBridge.*` 替代 `window.webkit.messageHandlers.*`)。
|
||
|
||
4. **不使用 Kotlin Multiplatform**:当前阶段直接用 Kotlin 重写,避免 KMP 的额外复杂度。未来如果需要共享逻辑层,可以逐步迁移。
|
||
|
||
5. **ViewPager2 替代 PageViewController**:Android 无 page curl 效果,使用 ViewPager2 + 自定义 PageTransformer 实现翻页动画(可选深度效果或滑动效果)。
|
||
|
||
---
|
||
|
||
## 文件映射索引(iOS → Android)
|
||
|
||
### 核心映射
|
||
|
||
| iOS Swift 文件 | Android Kotlin 文件 | 行数参考 |
|
||
|---------------|-------------------|---------|
|
||
| `RDEPUBParser.swift` + extensions | `EpubParser.kt` + extensions | ~600 |
|
||
| `RDEPUBModels.swift` | `EpubModels.kt` | ~200 |
|
||
| `RDEPUBPublication.swift` | `EpubPublication.kt` | ~150 |
|
||
| `RDEPUBJavaScriptBridge.swift` | `EpubJavaScriptBridge.kt` | ~400 |
|
||
| `RDEPUBStyleSheetBuilder.swift` | `StyleSheetBuilder.kt` | ~300 |
|
||
| `RDEPUBSearchEngine.swift` | `EpubSearchEngine.kt` | ~150 |
|
||
| `RDEPUBPaginator.swift` | `EpubPaginator.kt` | ~200 |
|
||
| `RDEPUBReadingSession.swift` | `ReadingSession.kt` | ~250 |
|
||
| `RDEPUBReaderController.swift` + extensions | `EpubReaderFragment.kt` + extensions | ~800 |
|
||
| `RDEPUBReaderContext.swift` | `ReaderContext.kt` | ~150 |
|
||
| `RDEPUBReaderRuntime.swift` | `ReaderRuntime.kt` | ~200 |
|
||
| 各 Coordinator 文件 | 对应 `*Coordinator.kt` | 各 ~100-200 |
|
||
| `RDEPUBWebView.swift` + extensions | `EpubWebView.kt` + extensions | ~500 |
|
||
| `RDReaderView.swift` + extensions | `PagerView.kt` + extensions | ~400 |
|
||
| `RDEPUBReaderSearchBarView.swift` | `SearchPanelView.kt` | ~500 |
|
||
| `RDEPUBReaderTopToolView.swift` | `TopToolBar.kt` | ~150 |
|
||
| `RDEPUBReaderBottomToolView.swift` | `BottomToolBar.kt` | ~200 |
|
||
| `RDEPUBReaderSettingsViewController.swift` | `SettingsSheetFragment.kt` | ~300 |
|
||
|
||
### 直接复用(不需翻译)
|
||
|
||
| 文件 | 说明 |
|
||
|------|------|
|
||
| `epub-bridge.js` | JS 核心桥接 |
|
||
| `WeReadApi.js` | JS API 门面 |
|
||
| `cssInjector.js` | CSS 注入工具 |
|
||
| `rangy-core.js` | Range 操作库 |
|
||
| `rangy-serializer.js` | Range 序列化 |
|
||
|
||
### 不迁移(Android 不需要)
|
||
|
||
| iOS 文件/目录 | 原因 |
|
||
|-------------|------|
|
||
| `EPUBTextRendering/`(18 个文件) | DTCoreText/CoreText 无 Android 等价物,统一用 WebView |
|
||
| `EPUBUI/TextPage/`(7 个文件) | 原生文字渲染层,Android 不需要 |
|
||
| `RDEPUBTextContentView.swift` | 同上 |
|
||
| `RDEPUBTextPageRenderView.swift` | 同上 |
|
||
| `RDEPUBWebDecorationOverlayView.swift` | Android WebView 的高亮由 JS 直接处理,不需要原生 overlay |
|