# 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
```
### 5.5 底部工具栏
对应 iOS `RDEPUBReaderBottomToolView`:
```xml
```
### 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, 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
fun saveBookmarks(bookId: String, bookmarks: List)
fun loadHighlights(bookId: String): List
fun saveHighlights(bookId: String, highlights: List)
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 = emptyList(),
val highlights: List = emptyList(),
val searchState: SearchState? = null,
)
class ReaderStateManager {
private val _uiState = MutableStateFlow(ReaderUiState())
val uiState: StateFlow = _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 |