Readium-2(簡(jiǎn)稱R2)是一個(gè)由Readium基金會(huì)開發(fā)的条霜,適用于Android與IOS平臺(tái)的閱讀器項(xiàng)目。與最同類的FBReader相比涵亏,最大的區(qū)別就是將電子書的解析與展示都交給了WebView來(lái)實(shí)現(xiàn)宰睡,并通過(guò)css與js來(lái)實(shí)現(xiàn)電子書的閱讀效果。
支持特性
- 支持EPUB 2.x 與 3.x
- 支持Readium LCP
- 支持CBZ格式
- 自定義樣式
- 夜間(深色)模式
- 支持翻頁(yè)模式與滾動(dòng)模式
- 電子書目錄
- 支持OPDS 1.x 與 2.0
- 支持FXL格式
- 支持RTL模式
先貼上項(xiàng)目的地址:https://github.com/readium/r2-testapp-kotlin
首先气筋,來(lái)大概介紹一下這個(gè)項(xiàng)目的優(yōu)缺點(diǎn)與適用場(chǎng)景拆内。
優(yōu)點(diǎn)
- 將電子書的解析與展示都交給了瀏覽器完成,無(wú)需手動(dòng)處理宠默。
- 由于項(xiàng)目開發(fā)時(shí)間較新麸恍,而且原生部分使用kotlin進(jìn)行開發(fā),不會(huì)有FBReader等項(xiàng)目難以編譯的問(wèn)題搀矫。
- 項(xiàng)目中沒(méi)有使用Native層的代碼抹沪。
缺點(diǎn)
- 性能相較基于基于原生的項(xiàng)目執(zhí)行效率上會(huì)差一些。
- 由于需要同時(shí)處理原生瓤球,JS融欧,CSS的代碼,可能會(huì)給開發(fā)和調(diào)試帶來(lái)一定的麻煩卦羡。
- 由于加載機(jī)制的限制噪馏,部分全局功能(如獲取全書總頁(yè)數(shù))難以實(shí)現(xiàn)。
- 在7.0或以下項(xiàng)目中展示效果會(huì)有問(wèn)題绿饵。(這一點(diǎn)可以通過(guò)css的適配來(lái)解決)
注:該項(xiàng)目依然在持續(xù)進(jìn)行更新逝薪,可能會(huì)在未來(lái)解決文中提到的部分問(wèn)題,詳情還是推薦關(guān)注該項(xiàng)目的Github主頁(yè)蝴罪。
適用場(chǎng)景
如果開發(fā)時(shí)間較為緊張董济,而且對(duì)于閱讀模塊的功能方面要求較為簡(jiǎn)單,對(duì)樣式支持上的要求較高要门,又能夠完成比較簡(jiǎn)單的js與css上的問(wèn)題的話虏肾,Readium-2是一個(gè)較為不錯(cuò)的選擇。
模塊結(jié)構(gòu)
代碼分析
對(duì)于一個(gè)閱讀器來(lái)說(shuō)欢搜,最主要無(wú)非兩個(gè)功能:對(duì)文件的解析與文本內(nèi)容的展示封豪。R2引入了NanoHttpd來(lái)直接在本地架設(shè)了一個(gè)輕量級(jí)的WebServer,然后將JS文件炒瘟,CSS文件吹埠,字體文件與電子書文件等等都加載到這個(gè)WebServer中,再由WebServer將這些文件打包為一個(gè)完整的Web然后交由WebView展示出來(lái)。下面就以Epub格式的電子書為例缘琅,分別從這兩個(gè)角度來(lái)看看R2在這兩方面具體是如何處理的粘都。
對(duì)文件的解析
首先,在onCreate方法中調(diào)用startServer方法啟動(dòng)本地服務(wù)器并加載部分基礎(chǔ)js文件刷袍,之后由EpubParser類來(lái)解析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
}
//標(biāo)記電子書格式為EPUB
container.rootFile.mimetype = mimetypeEpub
//通過(guò)解析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對(duì)象翩隧,即將所有的節(jié)點(diǎn)提取出來(lái)以便于之后的處理(OPF文件的結(jié)構(gòu)與xml文件幾乎一致)
xmlParser.parseXml(documentData.inputStream())
val epubVersion = xmlParser.root().attributes["version"]!!.toDouble()
//最后將核心OPF文件解析為Publication對(duì)象
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對(duì)象堆生,其中包含了整本書的metadata與目錄(即每一個(gè)目錄節(jié)點(diǎn)與對(duì)應(yīng)文件的映射關(guān)系)。
之后就是R2的重頭戲雷酪,WebServer的初始化與啟動(dòng)淑仆。先來(lái)看看Server類的構(gòu)造函數(shù):
class Server(port: Int) : AbstractServer(port)
abstract class AbstractServer(private var port: Int) : RouterNanoHTTPD("127.0.0.1", port)
所以Server其實(shí)就是一個(gè)擴(kuò)展過(guò)的RouterNanoHTTPD,限于篇幅哥力,就不向RouterNanoHTTPD的源碼進(jìn)行深究了糯景。在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"))
//通過(guò)對(duì)應(yīng)Handler將相應(yīng)文件添加進(jìn)本地服務(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對(duì)象的創(chuàng)建。addRoute方法將url與一個(gè)RouterNanoHTTPD.DefaultHandler的子類加入服務(wù)器中钞澳。之后在瀏覽器使用url訪問(wèn)本地服務(wù)器時(shí)怠惶,會(huì)調(diào)用Handler方法返回相應(yīng)的數(shù)據(jù)。下面來(lái)看看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) {
//對(duì)epub文件內(nèi)容進(jìn)行預(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方法會(huì)通過(guò)調(diào)用該方法獲取進(jìn)行過(guò)預(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方法中會(huì)調(diào)用contentFilters對(duì)象中的apply方法對(duì)內(nèi)容部分進(jìn)行預(yù)處理轧粟,如添加css樣式策治,引入js文件,引入字體文件等等兰吟。
到這里通惫,對(duì)于epub文件與本地服務(wù)器的預(yù)處理就基本完成了,之后將會(huì)跳轉(zhuǎn)到EpubActivity頁(yè)面進(jìn)行電子書的展示混蔼。
電子書的展示
在電子書閱讀的部分履腋,我相信直接來(lái)看EpubActivity的布局部分就能有一個(gè)很直觀的了解了:
<!-- 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>
通過(guò)布局可以看出,R2的閱讀部分很簡(jiǎn)單惭嚣,就是由多個(gè)WebView所組成的ViewPager遵湖,每一個(gè)WebView負(fù)責(zé)加載一個(gè)章節(jié)的內(nèi)容,因?yàn)閑pub格式中每個(gè)章節(jié)的內(nèi)容很類似于html晚吞,所以進(jìn)行一些預(yù)處理就可以直接在WebView中展示了延旧。而章節(jié)內(nèi)的翻頁(yè)與內(nèi)容跳轉(zhuǎn)的邏輯上的操作則交由WebView中的css與js部分來(lái)進(jìn)行處理,而章節(jié)間的切換的部分則是交給了ViewPager槽地。
對(duì)于WebView中操作的具體實(shí)現(xiàn)原理感興趣的朋友可以翻閱項(xiàng)目中的css與js文件迁沫,這里就不再展開了芦瘾。
那么以上就是對(duì)于Readium-2這個(gè)電子書項(xiàng)目的簡(jiǎn)單介紹了,希望能有更多人了解到這個(gè)項(xiàng)目集畅,也給有類似需求的開發(fā)者帶來(lái)一些幫助近弟。