Readium-2(簡稱R2)是一個由Readium基金會開發(fā)的,適用于Android與IOS平臺的閱讀器項目。與最同類的FBReader相比,最大的區(qū)別就是將電子書的解析與展示都交給了WebView來實現(xiàn),并通過css與js來實現(xiàn)電子書的閱讀效果。
支持特性
- 支持EPUB 2.x 與 3.x
- 支持Readium LCP
- 支持CBZ格式
- 自定義樣式
- 夜間(深色)模式
- 支持翻頁模式與滾動模式
- 電子書目錄
- 支持OPDS 1.x 與 2.0
- 支持FXL格式
- 支持RTL模式
先貼上項目的地址:https://github.com/readium/r2-testapp-kotlin
首先,來大概介紹一下這個項目的優(yōu)缺點與適用場景。
優(yōu)點
- 將電子書的解析與展示都交給了瀏覽器完成,無需手動處理。
- 由于項目開發(fā)時間較新,而且原生部分使用kotlin進行開發(fā),不會有FBReader等項目難以編譯的問題。
- 項目中沒有使用Native層的代碼。
缺點
- 性能相較基于基于原生的項目執(zhí)行效率上會差一些。
- 由于需要同時處理原生,JS,CSS的代碼,可能會給開發(fā)和調(diào)試帶來一定的麻煩。
- 由于加載機制的限制,部分全局功能(如獲取全書總頁數(shù))難以實現(xiàn)。
- 在7.0或以下項目中展示效果會有問題。(這一點可以通過css的適配來解決)
注:該項目依然在持續(xù)進行更新,可能會在未來解決文中提到的部分問題,詳情還是推薦關(guān)注該項目的Github主頁。
適用場景
如果開發(fā)時間較為緊張,而且對于閱讀模塊的功能方面要求較為簡單,對樣式支持上的要求較高,又能夠完成比較簡單的js與css上的問題的話,Readium-2是一個較為不錯的選擇。
模塊結(jié)構(gòu)

代碼分析
對于一個閱讀器來說,最主要無非兩個功能:對文件的解析與文本內(nèi)容的展示。R2引入了NanoHttpd來直接在本地架設(shè)了一個輕量級的WebServer,然后將JS文件,CSS文件,字體文件與電子書文件等等都加載到這個WebServer中,再由WebServer將這些文件打包為一個完整的Web然后交由WebView展示出來。下面就以Epub格式的電子書為例,分別從這兩個角度來看看R2在這兩方面具體是如何處理的。
對文件的解析
首先,在onCreate方法中調(diào)用startServer方法啟動本地服務(wù)器并加載部分基礎(chǔ)js文件,之后由EpubParser類來解析container.xml文件與核心OPF文件
EpubParse.parse
override fun parse(fileAtPath: String, title: String): PubBox? {
//獲取container.xml的輸出流
val container = try {
generateContainerFrom(fileAtPath)
} catch (e: Exception) {
Timber.e(e, "Could not generate container")
return null
}
val data = try {
container.data(containerDotXmlPath)
} catch (e: Exception) {
Timber.e(e, "Missing File : META-INF/container.xml")
return null
}
//標記電子書格式為EPUB
container.rootFile.mimetype = mimetypeEpub
//通過解析container.xml文件獲取核心OPF文件的路徑
container.rootFile.rootFilePath = getRootFilePath(data)
val xmlParser = XmlParser()
val documentData = try {
container.data(container.rootFile.rootFilePath)
} catch (e: Exception) {
Timber.e(e, "Missing File : ${container.rootFile.rootFilePath}")
return null
}
//將核心OPF文件解析為XmlParser對象,即將所有的節(jié)點提取出來以便于之后的處理(OPF文件的結(jié)構(gòu)與xml文件幾乎一致)
xmlParser.parseXml(documentData.inputStream())
val epubVersion = xmlParser.root().attributes["version"]!!.toDouble()
//最后將核心OPF文件解析為Publication對象
val publication = opfParser.parseOpf(xmlParser, container.rootFile.rootFilePath, epubVersion)
?: return null
val drm = container.scanForDrm()
parseEncryption(container, publication, drm)
parseNavigationDocument(container, publication)
parseNcxDocument(container, publication)
/*
* This might need to be moved as it's not really about parsing the Epub
* but it sets values needed (in UserSettings & ContentFilter)
*/
setLayoutStyle(publication)
container.drm = drm
return PubBox(publication, container)
}
在上面的代碼中,解析的邏輯上還是比較常規(guī)的,其中最關(guān)鍵的部分就是生成了Publication對象,其中包含了整本書的metadata與目錄(即每一個目錄節(jié)點與對應(yīng)文件的映射關(guān)系)。
之后就是R2的重頭戲,WebServer的初始化與啟動。先來看看Server類的構(gòu)造函數(shù):
class Server(port: Int) : AbstractServer(port)
abstract class AbstractServer(private var port: Int) : RouterNanoHTTPD("127.0.0.1", port)
所以Server其實就是一個擴展過的RouterNanoHTTPD,限于篇幅,就不向RouterNanoHTTPD的源碼進行深究了。在Server創(chuàng)建完成后,要將電子書的基本信息載入Server中:
Server.addEpub
fun addEpub(publication: Publication, container: Container, fileName: String, userPropertiesPath: String?) {
val fetcher = Fetcher(publication, container, userPropertiesPath, customResources)
//處理link中的額外字段
addLinks(publication, fileName)
publication.addSelfLink(fileName, URL("$BASE_URL:$port"))
//通過對應(yīng)Handler將相應(yīng)文件添加進本地服務(wù)器中
if (containsMediaOverlay) {
addRoute(fileName + MEDIA_OVERLAY_HANDLE, MediaOverlayHandler::class.java, fetcher)
}
addRoute(fileName + JSON_MANIFEST_HANDLE, ManifestHandler::class.java, fetcher)
addRoute(fileName + MANIFEST_HANDLE, ManifestHandler::class.java, fetcher)
addRoute(fileName + MANIFEST_ITEM_HANDLE, ResourceHandler::class.java, fetcher)
addRoute(JS_HANDLE, JSHandler::class.java, resources)
addRoute(CSS_HANDLE, CSSHandler::class.java, resources)
addRoute(FONT_HANDLE, FontHandler::class.java, fonts)
}
private fun addLinks(publication: Publication, filePath: String) {
containsMediaOverlay = false
//判斷電子書是否支持多媒體內(nèi)容(如音頻,視頻等)
for (link in publication.otherLinks) {
if (link.rel.contains("media-overlay")) {
containsMediaOverlay = true
link.href = link.href?.replace("port", "127.0.0.1:$listeningPort$filePath")
}
}
}
在上述代碼中,值得關(guān)注的有addRoute(String url, Class<?> handler, Object... initParameter)方法與Fetcher對象的創(chuàng)建。addRoute方法將url與一個RouterNanoHTTPD.DefaultHandler的子類加入服務(wù)器中。之后在瀏覽器使用url訪問本地服務(wù)器時,會調(diào)用Handler方法返回相應(yīng)的數(shù)據(jù)。下面來看看Fetcher類的部分代碼:
class Fetcher(var publication: Publication, var container: Container, private val userPropertiesPath: String?, customResources: Resources? = null) {
// …………
private fun getContentFilters(mimeType: String?, customResources: Resources? = null): ContentFilters {
return when (mimeType) {
//對epub文件內(nèi)容進行預(yù)處理后
"application/epub+zip", "application/oebps-package+xml" -> ContentFiltersEpub(userPropertiesPath, customResources)
"application/vnd.comicbook+zip", "application/x-cbr" -> ContentFiltersCbz()
else -> throw Exception("Missing container or MIMEtype")
}
}
//ResourceHandler類中的get方法會通過調(diào)用該方法獲取進行過預(yù)處理后的書籍內(nèi)容的InputStream
fun dataStream(path: String): InputStream {
var inputStream = container.dataInputStream(path)
inputStream = contentFilters?.apply(inputStream, publication, container, path) ?: inputStream
return inputStream
}
在dataStream方法中會調(diào)用contentFilters對象中的apply方法對內(nèi)容部分進行預(yù)處理,如添加css樣式,引入js文件,引入字體文件等等。
到這里,對于epub文件與本地服務(wù)器的預(yù)處理就基本完成了,之后將會跳轉(zhuǎn)到EpubActivity頁面進行電子書的展示。
電子書的展示
在電子書閱讀的部分,我相信直接來看EpubActivity的布局部分就能有一個很直觀的了解了:
<!-- activity_r2_viewpager.xml -->
<androidx.constraintlayout.widget.ConstraintLayout>
<org.readium.r2.navigator.pager.R2ViewPager />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- viewpager_fragment_epub.xml -->
<androidx.constraintlayout.widget.ConstraintLayout>
<org.readium.r2.navigator.R2WebView/>
</androidx.constraintlayout.widget.ConstraintLayout>
通過布局可以看出,R2的閱讀部分很簡單,就是由多個WebView所組成的ViewPager,每一個WebView負責加載一個章節(jié)的內(nèi)容,因為epub格式中每個章節(jié)的內(nèi)容很類似于html,所以進行一些預(yù)處理就可以直接在WebView中展示了。而章節(jié)內(nèi)的翻頁與內(nèi)容跳轉(zhuǎn)的邏輯上的操作則交由WebView中的css與js部分來進行處理,而章節(jié)間的切換的部分則是交給了ViewPager。
對于WebView中操作的具體實現(xiàn)原理感興趣的朋友可以翻閱項目中的css與js文件,這里就不再展開了。
那么以上就是對于Readium-2這個電子書項目的簡單介紹了,希望能有更多人了解到這個項目,也給有類似需求的開發(fā)者帶來一些幫助。