# 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 |