ReadViewSDK/Doc/Android EPUB 阅读器实施方案.md
shen 6f75b083f7 feat: EPUB 阅读器搜索、选中注释、书签 chrome 状态及大量重构优化
- 新增 RDEPUBReaderSearchCoordinator 与 RDEPUBSelectionState 管理搜索和选中状态
- 新增 BookmarkChromeStateTests、NavigationBackwardTests、SelectionAnnotateTests 等 UI 测试
- 新增多个边界测试 epub 样本(损坏结构、空归档、缺失文件、流式外链验证)
- 重构阅读器 chrome 状态管理,统一 tool bar 与 search bar 交互
- 优化大书分页缓存策略(RDEPUBChapterSummaryDiskCache、RDEPUBPageCountCache)
- 移除废弃的 RDEPUBLocationConverter 和 RDEPUBPageBreakPolicy
- 更新 epub-bridge.js 与 JS bridge 通信协议
- 全面更新现有 UI 测试以适配新的 helper 和状态管理
2026-06-13 22:48:56 +08:00

22 KiB
Raw Blame History

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
  • 最低 API24Android 7.0
  • 目标 API34
  • 构建工具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 公共依赖

// 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 使用 XMLParserSAXAndroid 使用 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 渲染,不实现 EPUBTextRenderingDTCoreText 的 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 实现

// 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/ 自定义协议:

// 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 RDReaderViewUICollectionView + UIPageViewController

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

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

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

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

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

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

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

// 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 的手动状态同步:

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

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

// 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 替代 PageViewControllerAndroid 无 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