Android:FBReader解析Epub总结

前言

1.在FBReader使用前,推荐大家先看下https://www.jianshu.com/p/d471f453ce0a,里面给我们讲解了EPUB文件结构,推荐要用这个FBReader的大家先去看一看!

2.支持电子书格式:EPUB、kindle (mobipocket)、fb2、rtf、html、microsoft doc、txt、PDF(需要PDF插件)、DJVU(需要DjVU插件)、CBR和CBZ漫画(需要comic插件)
支持直接阅读zip文件
提供直接访问许多网络电子书图书馆和商店
支持自定义OPDS目录的添加
与一些流行的字典如ColorDict, SlovoEd, Fora等整合
可以使用外部TrueType/OpenType字体
29种语言的本地化
包含16种语言的断字模式
重排,亮度调节,检索,主题切换,夜间,日常模式,笔记,书签,目录,字体大小,长按复制

一、代码导入

https://github.com/geometer/FBReaderJ 这个地址上就是fbreader的java项目,我们把它下载到我们本地。

image.png

开始导入AS

改成你自己的Studio版本号
删除.gradle,.idea
gradle改成你自己的gradle版本
gradle.properties 改为自己的sdk路径和ndk路径

image.png

二、运行

编译项目so文件
修改FBReaderIntents中包名常量DEFAULT_PACKAGE为自己项目的包名

三、项目结构

image.png
image.png
image.png
image.png
image.png

image.png

三、项目解析

(1)、AmbilWarna:
Android Color Picker application using AmbilWarna Color Picker Library
背景颜色选择器,用在设置背景颜色时,以及标签背景编辑时使用。
可以提取为一个模块。放到最后再提取,先整理主工程。

移植遇到的坑比较多:
使用AmbilWarnaKotak和AmbilWarnaPrefWidgetView 的xml中的包名需要修改;
原来的为
yuku.ambilwarna.AmbilWarnaKotak
yuku.ambilwarna.widget.AmbilWarnaPrefWidgetView
修改后为,
com.thridapp.widget.AmbilWarnaKotak
com.thridapp.widget.AmbilWarnaPrefWidgetView

(2)、android-filechooser:
文件选择器,用来进行文件选择。
org.geometerplus.android.util.FileChooserUtil 需要关注,其它好像用途不大,项目中其它文件基本没有在使用。
包名为:group.pals.android.lib.ui.filechooser
可以暂时保留。

(3)、drag-sort-listview:
拖动排序列表。
只有CatalogManagerActivity(目录管理界面)中使用了DragSortListView ,可以暂时忽略这个界面,RN端来实现。
所以此模块可以不用集成。

包名为:com.mobeta.android.dslv
此模块可以删除。对应“在线书库” 中的“管理书库”菜单中的列表排序。CatalogManagerActivity 为对应的activity,可以将其删除。

(4)、superToasts:
特殊的toast控件,在工程中使用的地方较多。
可以提取为一个模块,注意其中无用的文件可以删除掉。
包名为:com.github.johnpersano.supertoasts
此工程可以暂时保留。

2、jar库
(1)、httpmime-4.2.5.jar
HttpClient 是 Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

(2)、json-simple-1.1.1.jar
JSON.simple是一个很简单的JSON处理,读取和写JSON数据的JAVA库,它完全兼容JSON的标准(RFC4627)。你可以用JSON.simple来编码或解码JSON文本。

(3)、LingvoIntegration_2.5.2.12.jar
语言集成包。应用是提供多语言服务的。

(4)、nanohttpd-2.0.5.jar
NanoHTTPD是专为嵌入在其他Java应用程序中使用的一个轻量级HTTP服务器,它只有一个Java文件。支持 GET, POST, PUT, HEAD 和 DELETE 请求,支持文件上传,占用内存很小。可轻松定制临时文件使用和线程模型。

(5)、open-dictionary-api-1.2.1.jar
The Open Dictionary API Alliance (ODAA) is an open community of dictionary publishers aiming to create new possibilities for millions of people using electronic dictionaries on their mobile devices all over the world.
打开词典API联盟(ODAA)是字典出版商旨在全世界范围内,创造一个为全世界数百万人在他们的移动设备上使用电子词典的新可能性。

(6)、pdfparse.jar
PdfParser, a standalone PHP library, provides various tools to extract data from a PDF file.
PdfParser,一个独立的PHP库,提供了各种工具,从PDF文件中提取数据。

3、jni
注意,
原始编译选项为,APP_ABI := armeabi armeabi-v7a x86 mips arm64-v8a mips64 x86_64
如果有的手机无法正常运行,可以考虑是否少so文件。

(1)、linebreak :
Liblinebreak is an implementation of the line breaking algorithm as described in Unicode 6.0.0 Standard Annex 14, Revision 26. It breaks lines that contain Unicode characters. It is designed to be used in a generic text renderer. FBReader is one real-world example, and you may also check some simple sample code, like showbreak and breaktext.
应该是用来换行排版使用的
此库比较有用,在LineBreaker.java中进行调用,对应native层的LineBreaker.cpp,ZLTextParagraphCursor.java中会调用到。

(2)、Expat :
Expat是一个用C语言开发的、用来解析XML文档的开发库,它最初是开源的、Mozilla项目下的一个XML解析器。
expat是使用C所写的XML解释器,采用流的方式来解析XML文件,并且基于事件通知型来调用分析到的数据,并不需要把所有XML文件全部加载到内存里,这样可以分析非常大的XML文件。由于expat库是由XML的主要负责人James Clark来实现的,因此它是符合W3C的XML标准的。

(3)、DeflatingDecompressor :
应该是进行压缩的。
DeflatingDecompressor.java中进行调用,对应native层的DeflatingDecompressor.cpp文件。

(4)、NativeFormats
用来对不同格式进行兼容处理的,应该包括,
css,doc,fb2,html,xhtml, oeb,pdb ,rtf ,txt等格式。
提供不同格式的文件以插件方式来支持。
在PluginCollection.java中调用,对应native的JavaPluginCollection.cpp文件。
PluginCollection 应该是插件管理器。

4、模块
(1)、zip-amse
包路径:org.amse.ys.zip
功能需要保留,但是包名要替换。注意涉及到底层DeflatingDecompressor 库。

(2)、util
包路径:org.fbreader.util
功能需要保留,但是包名要替换。

(3)、text-formats
全部为jni代码,包括两部分,expat-2.0.1 和NativeFormats,应该是对文字字符进行格式化。
最终会编译成.so或者.a库,可以不用修改,但是对外提供的natvie接口名称需要修改。

(4)、resources
包路径:assets.resources.application和assets.resources.zlibrary
多国语言字符串资源,放在assets目录下,可以只保留英文和简体中文。

(5)、common
涉及到的包路径:
com.paragon.dictionary.fbreader
org.fbreader
org.geometerplus
可以和主工程合到一起,包括assets目录下的文件。

(6)、api
涉及到的包路径:
api.src.main.java.org.geometerplus.android
api.src.main.java.org.geometerplus.fbreader
api.src.main.java.org.geometerplus.zlibrary
可以和主工程合到一起。

(7)、app主工程
涉及到的包路径:
java.org.geometerplus.android
java.org.geometerplus.fbreader
java.org.geometerplus.zlibrary

5、assets目录
(1)、data
data.premium ,licences.html提供licences和购买信息。
data.whatsnew介绍新特性。

(2)、default
默认的按键,插件配置信息
tapzones,keymap.xml提供按键映射的xml文件
styles.css,styles.xml提供默认值配置文件
plugins.xml提供插件对应的包名信息

(3)、dictionaries
词典相关的配置信息
bitknights.xml 提供bitknights 官方网站提供多种词典下载的地址
main.xml提供多种词典打开的方式

(4)、encodings
编码方式设置信息
Encodings.xml列出支持的编码方式

(5)、formats
包括,
formats.fb2
formats.html
formats.xhtml 三个目录。
为NativeFormats 的jni工程提供配置信息。

(6)、hyphenationPatterns
提供断字模式,模块ZLTextTeXHyphenator.java会调用到。

(7)、languagePatterns
语言不同编码模式,languagePatterns.java和ZLLanguageList.cpp会用到。

(8)、resources
多国语言字符串资源。

(9)、wallpapers
预置了几种墙纸文件,为jpg格式文件。

6、AndroidManifest文件分析
如果只需要阅读引擎的话,所有的activity都应该清理掉,下面只分析一下Service和Receiver。

<!-- receiver -->
<!--BookDownloaderService 中调用,和下载相关的广播-->
<receiver android:name="com.laoxiao79.test.android.reader.network.ListenerCallback"
    android:process=":networkLibrary">
    <intent-filter>
        <action android:name="android.fbreader.action.network.SIGNIN"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</receiver>

<!-- service -->
<!--ApiClientImplementation 类隐式启动, 没有找到启动ApiClientImplementation地方,所以此类应该是未用-->
<service android:name="com.laoxiao79.test.android.reader.api.ApiService"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.fbreader.action.API"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</service>

<!--BookCollectionShadow 中隐式启动,用来创建,访问数据库,存储book信息到本地-->
<service android:name="com.laoxiao79.test.android.reader.libraryService.LibraryService"
    android:launchMode="singleTask"
    android:process=":libraryService">
    <intent-filter>
        <action android:name="android.fbreader.action.LIBRARY_SERVICE"/>
    </intent-filter>
</service>

<!--ConfigShadow 类隐式启动,只有在ZLAndroidApplication中的一个实例-->
<service android:name="com.laoxiao79.test.android.reader.config.ConfigService"
    android:launchMode="singleTask"
    android:process=":configService">
    <intent-filter>
        <action android:name="android.fbreader.action.CONFIG_SERVICE"/>
    </intent-filter>
</service>

<!--和官网同步服务相关-->
<service android:name="com.laoxiao79.test.android.reader.sync.SyncService"
    android:launchMode="singleTask"
    android:process=":synchroniser">
    <intent-filter>
        <action android:name="android.fbreader.action.sync.START"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.fbreader.action.sync.QUICK_SYNC"/>
    </intent-filter>
</service>

<!--BookDownloader下载相关的activity-->
<service android:name="com.laoxiao79.test.android.reader.network.BookDownloaderService"
    android:launchMode="singleTask"
    android:process=":networkLibrary"
    android:exported="false"/>

<!--后台网络访问-->
<service android:name="com.laoxiao79.test.android.reader.httpd.DataService"
    android:launchMode="singleTask"
    android:process=":dataService"
    android:exported="false"/>

<!--FileChooserActivity 中用到,和选择文件有关-->
<service android:name="group.pals.android.lib.ui.filechooser.services.LocalFileProvider"
    android:exported="false"/>

FBReader点击执行流程

在清单文件,可以发现FBReader的主Activity即为FBReader,可谓是直截了当的命名。那我们就进入FBReader一探究竟。
嗯.... 1053行.... 再看看里面,奇奇怪怪各种变量、不认识的类、不知道干啥的方法,看的着实让人头皮发麻,那索性去看看布局文件,这总算可以吧?不多说,看内容:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<org.geometerplus.zlibrary.ui.android.view.ZLAndroidWidget
    android:id="@+id/main_view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:focusable="true"
    android:scrollbars="vertical"
    android:scrollbarAlwaysDrawVerticalTrack="true"
    android:fadeScrollbars="false"
/>
</RelativeLayout>

复制代码
很简单,也很清晰明了,就一个核心 ZLAndroidWidget,看起来这个核心的控件好像是显示和操作的最终也是唯一载体,这个时候再回看一下程序启动的页面,不免有两个疑问:

布局文件中没有设置背景图,但是为什么显示的页面看着是有
页面最下方有一个黑色线条,怎么出现的,又有什么作用呢
这两个疑问暂时先放在这里,我们继续往后看。接下来,我们就要去操作app打开一本书了,还记得我们之前对首页划分的区域吗。我们依次点击这9个区域,会发现只有当点击(1,2)这个区域的时候才能够弹出来操作菜单:

刚才我们看过布局文件,知道了FBReader这个Activity的布局中只有一个核心控件ZLAndroidWidget,而且从这个特殊行为(只有点 1,2 区域才弹出菜单)来看,应该是在触摸事件的处理过程中,判断了用户点击的区域才做出相应的行为,到底是不是这样呢?我们直接进入ZLAndroidWidget,去一探究竟。

ZLAndroidWidget对点击区域的特殊处理
我们直接来看它的onTouchEvent方法,鉴于关注的是点击事件,直接瞅准
action up :

case MotionEvent.ACTION_UP:
if (myPendingDoubleTap) {
    //double click 
    view.onFingerDoubleTap(x, y);
} else if (myLongClickPerformed) {
    // long press
    view.onFingerReleaseAfterLongPress(x, y);
} else {
    if (myPendingLongClickRunnable != null) {
        removeCallbacks(myPendingLongClickRunnable);
        myPendingLongClickRunnable = null;
    }
    if (myPendingPress) {
        if (view.isDoubleTapSupported()) {
            if (myPendingShortClickRunnable == null) {
                myPendingShortClickRunnable = new ShortClickRunnable();
            }
            postDelayed(myPendingShortClickRunnable, ViewConfiguration.getDoubleTapTimeout());
        } else {
            //single tap !
            view.onFingerSingleTap(x, y);
        }
    } else {
        view.onFingerRelease(x, y);
    }
}
myPendingDoubleTap = false;
myPendingPress = false;
myScreenIsTouched = false;
break;

复制代码
可以看到其对各种触摸事件的判断,有双击、长按和单击,这里我们去看单击事件的处理onFingerSingleTap(x,y),点进去后发现其定义再ZLView,唯一实现在FBView。点击(2,1)区域,断点跟进去之后可以发现,最终触发的方法是进入onFingerSingleTapLastResort(x,y):

public void onFingerSingleTap(int x, int y) {
    // 上面的代码省略...   
    onFingerSingleTapLastResort(x, y);
}

复制代码
进入onFingerSingleTapLastResort(x,y),这里需要注意一个点,判断了是否支持双击操作isDoubleTapSupported(),并且根据结果判断传递到后续的tap类型,这有什么用呢?暂且先不管,先看:

private void onFingerSingleTapLastResort(int x, int y) {
    myReader.runAction(getZoneMap().getActionByCoordinates(
        x, y, getContextWidth(), getContextHeight(),
        isDoubleTapSupported() ? TapZoneMap.Tap.singleNotDoubleTap : TapZoneMap.Tap.singleTap
        ), x, y);
}

复制代码
这里出现了一个runAction,进入一瞧:

public final void runAction(String actionId, Object ... params) {
    //从map中依据actionId去找到对应的action  那么map是什么时候存储这些actionId的呢?
    final ZLAction action = myIdToActionMap.get(actionId);
    if (action != null) {
        // action找到了,执行action并把参数传过去
        action.checkAndRun(params);
    }
}

复制代码
再看checkAndRun,这个时候发现了一个新的基类ZLAction:

static abstract public class ZLAction {
    public boolean isVisible() {
        return true;
    }
    public boolean isEnabled() {
        return isVisible();
    }
    public Boolean3 isChecked() {
        return Boolean3.UNDEFINED;
    }
    public final boolean checkAndRun(Object ... params) {
        if (isEnabled()) {//默认true
            run(params);
            return true;
        }
        return false;
    }
    abstract protected void run(Object ... params);
}

复制代码
现在我们知道,onFingerSingleTapLastResort这个方法其实是执行了actionId对应的action的run方法,并且传递过去的参数是x和y(触摸坐标),那么这个actionId是怎么来的呢?对应的action又干了什么呢?

针对弹出菜单的单击事件,actionId是在哪定义的,又怎么一步步获取到的呢:
根据之前onFingerSingleTapLastResort方法分步分析:

private void onFingerSingleTapLastResort(int x, int y) {
    myReader.runAction(getZoneMap().getActionByCoordinates(...);
}

复制代码
1.getZoneMap获取TapZoneMap

private TapZoneMap getZoneMap() {
    final PageTurningOptions prefs = myReader.PageTurningOptions;
    String id = prefs.TapZoneMap.getValue();
    if ("".equals(id)) {
        id = prefs.Horizontal.getValue() ? "right_to_left" : "up";
    }
    if (myZoneMap == null || !id.equals(myZoneMap.Name)) {
        myZoneMap = TapZoneMap.zoneMap(id);
    }
    return myZoneMap;
}

复制代码
2.翻页设置PageTurningOptions的TapZoneMap默认值为"":

public class PageTurningOptions {
    public static enum FingerScrollingType {
        byTap, //点击翻页
        byFlick, //滑动翻页
        byTapAndFlick // 点击和滑动翻页
    }
    //滑动方式 默认可点击翻页也可滑动翻页
    public final ZLEnumOption<FingerScrollingType> FingerScrolling =
        new ZLEnumOption<FingerScrollingType>("Scrolling", "Finger", FingerScrollingType.byTapAndFlick);
    //默认动画方式
    public final ZLEnumOption<ZLView.Animation> Animation =
        new ZLEnumOption<ZLView.Animation>("Scrolling", "Animation", ZLView.Animation.slide);
    //默认动画速度
    public final ZLIntegerRangeOption AnimationSpeed =
        new ZLIntegerRangeOption("Scrolling", "AnimationSpeed", 1, 10, 7);
    //横向滑动 false为竖向滑动
    public final ZLBooleanOption Horizontal =
        new ZLBooleanOption("Scrolling", "Horizontal", true);
    //点击区域规则约束
    public final ZLStringOption TapZoneMap =
        new ZLStringOption("Scrolling", "TapZoneMap", "");
}

复制代码
3.由于默认值为"",那么生成TapZoneMap时传入的id为"right_to_left"

4.TapZoneMap创建时根据传入id做了什么:

private TapZoneMap(String name) {
    Name = name;
    myOptionGroupName = "TapZones:" + name;
    myHeight = new ZLIntegerRangeOption(myOptionGroupName, "Height", 2, 5, 3);// 默认值3 最小 2 最大 5
    myWidth = new ZLIntegerRangeOption(myOptionGroupName, "Width", 2, 5, 3);// 默认值3 最小 2 最大5
    // 最小分块为 2*2  最大为 5*5
    // 加载名字为name的资源文件 !!
    final ZLFile mapFile = ZLFile.createFileByPath(
        "default/tapzones/" + name.toLowerCase() + ".xml"
    );
    XmlUtil.parseQuietly(mapFile, new Reader());//此处解析该资源文件
}
 
private class Reader extends DefaultHandler {
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
    try {
        if ("zone".equals(localName)) {
            final Zone zone = new Zone(
                Integer.parseInt(attributes.getValue("x")),
                Integer.parseInt(attributes.getValue("y"))
            );
            final String action = attributes.getValue("action");//取出action
            final String action2 = attributes.getValue("action2");//取出action2
            if (action != null) {
                myZoneMap.put(zone, createOptionForZone(zone, true, action));
            }
            if (action2 != null) {
                myZoneMap2.put(zone, createOptionForZone(zone, false, action2));
            }
        } else if ("tapZones".equals(localName)) {
            final String v = attributes.getValue("v");
            // 获取xml中定义的横向分块数
            if (v != null) {
                myHeight.setValue(Integer.parseInt(v));
            }
            final String h = attributes.getValue("h");
            // 获取xml中定义的竖向分块数
            if (h != null) {
                myWidth.setValue(Integer.parseInt(h));
            }
        }
    } catch (Throwable e) {
    }
    }
}

复制代码
5.资源文件位置,和其内容定义:

我们知道默认加载的资源为right_to_left,那么就进去看一下:

这里的区域划分,再回看一下上面区域划分的图,找到我们点击能弹出菜单的区域(1,2),可以看到定义了action2="menu",似乎跟我们想象的匹配起来了啊。而且可以发现有些区域定义了两个,action和action2,那么为什么有的会有两个呢?这两个是什么时候用的呢?带着疑问我们继续探索。

6.前面几步已经获取到了TapZoneMap,接着看其方法getActionByCoordinates:

public String getActionByCoordinates(int x, int y, int width, int height, Tap tap) {
    //忽略一部分代码...
    // 这里myWidth和myHeight的默认值为3(3*3),与划分的区域块数相同 而且在解析xml的时候还会设置一下,使其与xml中定义的数值一致
    // 因此相当于 x / (width / 3) 横向第几块   y / (height / 3) 竖向第几块
    return getActionByZone(myWidth.getValue() * x / width, myHeight.getValue() * y / height, tap);
}
复制代码
继续跟进到getActionByZone:

public String getActionByZone(int h, int v, Tap tap) {
    final ZLStringOption option = getOptionByZone(new Zone(h, v), tap);
    return option != null ? option.getValue() : null;
}

复制代码
最后进入getOptionByZone:

private ZLStringOption getOptionByZone(Zone zone, Tap tap) {
    switch (tap) {
        default:
        return null;
        case singleTap:
            {
                final ZLStringOption option = myZoneMap.get(zone);
                return option != null ? option : myZoneMap2.get(zone);
            }
        case singleNotDoubleTap:
            return myZoneMap.get(zone);
        case doubleTap:
            return myZoneMap2.get(zone);
    }
}

复制代码
还记得之前有个方法对是否支持双击的判断么。支持双击tap则为singleNotDoubleTap,否则为singleTap,而且为singleTap时如果action为空,那么就取action2的值。至此,我们总算是得到了对应的actionId = "menu"。

二、有了“有效操作”对应的actionId,怎么把它变成真正的行动呢?
通过上面的追踪,我们已经得到了最终的指令:actionId。针对于actionId,又是怎么识别和采取实际行动的呢?我们接着往下看。

这次我们进入主Activity FBReader,从生命周期起始的onCreate看起:

@Override
protected void onCreate(Bundle icicle) {
    super.onCreate(icicle);
        //省略部分代码...
        //本地书柜    
    myFBReaderApp.addAction(ActionCode.SHOW_LIBRARY, new ShowLibraryAction(this, myFBReaderApp));
    //阅读相关设置
    myFBReaderApp.addAction(ActionCode.SHOW_PREFERENCES, new ShowPreferencesAction(this, myFBReaderApp));
    //书籍信息
    myFBReaderApp.addAction(ActionCode.SHOW_BOOK_INFO, new ShowBookInfoAction(this, myFBReaderApp));
    //本书目录
    myFBReaderApp.addAction(ActionCode.SHOW_TOC, new ShowTOCAction(this, myFBReaderApp));
    //我的书签
    myFBReaderApp.addAction(ActionCode.SHOW_BOOKMARKS, new ShowBookmarksAction(this, myFBReaderApp));
    //在线书库
    myFBReaderApp.addAction(ActionCode.SHOW_NETWORK_LIBRARY, new ShowNetworkLibraryAction(this, myFBReaderApp));
    //显示菜单
    myFBReaderApp.addAction(ActionCode.SHOW_MENU, new ShowMenuAction(this, myFBReaderApp));
    //显示当前阅读进度pop
    myFBReaderApp.addAction(ActionCode.SHOW_NAVIGATION, new ShowNavigationAction(this, myFBReaderApp));
    //内容查找
    myFBReaderApp.addAction(ActionCode.SEARCH, new SearchAction(this, myFBReaderApp));
    //共享书籍
    myFBReaderApp.addAction(ActionCode.SHARE_BOOK, new ShareBookAction(this, myFBReaderApp));
    //显示长按选中区域
    myFBReaderApp.addAction(ActionCode.SELECTION_SHOW_PANEL, new SelectionShowPanelAction(this, myFBReaderApp));
    //隐藏长按选中区域
    myFBReaderApp.addAction(ActionCode.SELECTION_HIDE_PANEL, new SelectionHidePanelAction(this, myFBReaderApp));
    //复制选中内容到剪切板
    myFBReaderApp.addAction(ActionCode.SELECTION_COPY_TO_CLIPBOARD, new SelectionCopyAction(this, myFBReaderApp));
    //分享选中内容
    myFBReaderApp.addAction(ActionCode.SELECTION_SHARE, new SelectionShareAction(this, myFBReaderApp));
    //字典查询选中内容
    myFBReaderApp.addAction(ActionCode.SELECTION_TRANSLATE, new SelectionTranslateAction(this, myFBReaderApp));
    //在选中位置添加书签
    myFBReaderApp.addAction(ActionCode.SELECTION_BOOKMARK, new SelectionBookmarkAction(this, myFBReaderApp));
    //点击处内容类型为ZLTextRegion.ExtensionFilter时触发此action
    myFBReaderApp.addAction(ActionCode.DISPLAY_BOOK_POPUP, new DisplayBookPopupAction(this, myFBReaderApp));
    //点击处可跳转指定位置如目录
    myFBReaderApp.addAction(ActionCode.PROCESS_HYPERLINK, new ProcessHyperlinkAction(this, myFBReaderApp));
    //点击处为视频
    myFBReaderApp.addAction(ActionCode.OPEN_VIDEO, new OpenVideoAction(this, myFBReaderApp));
    //隐藏toast
    myFBReaderApp.addAction(ActionCode.HIDE_TOAST, new HideToastAction(this, myFBReaderApp));
    //点击返回按钮时,弹出菜单
    myFBReaderApp.addAction(ActionCode.SHOW_CANCEL_MENU, new ShowCancelMenuAction(this, myFBReaderApp));
    //开始屏幕(会打开帮助文档)
    myFBReaderApp.addAction(ActionCode.OPEN_START_SCREEN, new StartScreenAction(this, myFBReaderApp));
    //设置屏幕朝向跟随系统当前
    myFBReaderApp.addAction(ActionCode.SET_SCREEN_ORIENTATION_SYSTEM, new SetScreenOrientationAction(this, myFBReaderApp, ZLibrary.SCREEN_ORIENTATION_SYSTEM));
    //设置屏幕朝向跟随陀螺仪
    myFBReaderApp.addAction(ActionCode.SET_SCREEN_ORIENTATION_SENSOR, new SetScreenOrientationAction(this, myFBReaderApp, ZLibrary.SCREEN_ORIENTATION_SENSOR));
    //设置屏幕竖直朝向
    myFBReaderApp.addAction(ActionCode.SET_SCREEN_ORIENTATION_PORTRAIT, new SetScreenOrientationAction(this, myFBReaderApp, ZLibrary.SCREEN_ORIENTATION_PORTRAIT));
    //设置屏幕水平朝向
    myFBReaderApp.addAction(ActionCode.SET_SCREEN_ORIENTATION_LANDSCAPE, new SetScreenOrientationAction(this, myFBReaderApp, ZLibrary.SCREEN_ORIENTATION_LANDSCAPE));
    if (getZLibrary().supportsAllOrientations()) {
            //可反向竖直
        myFBReaderApp.addAction(ActionCode.SET_SCREEN_ORIENTATION_REVERSE_PORTRAIT, new SetScreenOrientationAction(this, myFBReaderApp, ZLibrary.SCREEN_ORIENTATION_REVERSE_PORTRAIT));
        //可反向水平
        myFBReaderApp.addAction(ActionCode.SET_SCREEN_ORIENTATION_REVERSE_LANDSCAPE, new SetScreenOrientationAction(this, myFBReaderApp, ZLibrary.SCREEN_ORIENTATION_REVERSE_LANDSCAPE));
    }
    //帮助
    myFBReaderApp.addAction(ActionCode.OPEN_WEB_HELP, new OpenWebHelpAction(this, myFBReaderApp));
    //安装插件
    myFBReaderApp.addAction(ActionCode.INSTALL_PLUGINS, new InstallPluginsAction(this, myFBReaderApp));
    //切换日间模式
    myFBReaderApp.addAction(ActionCode.SWITCH_TO_DAY_PROFILE, new SwitchProfileAction(this, myFBReaderApp, ColorProfile.DAY));
    //切换夜间模式
    myFBReaderApp.addAction(ActionCode.SWITCH_TO_NIGHT_PROFILE, new SwitchProfileAction(this, myFBReaderApp, ColorProfile.NIGHT));
        //省略部分代码...
}

复制代码
再来看看myFBReaderApp的addAction方法:

public final void addAction(String actionId, ZLAction action) {
    myIdToActionMap.put(actionId, action);
}

复制代码
很明显,在onCreate的时候,已经将这些可操作行为id和对应的action存储到了myFBReaderApp的myIdToActionMap,还记得之前单击事件之后调用的runAction吗:

public final void runAction(String actionId, Object ... params) {
    final ZLAction action = myIdToActionMap.get(actionId);
    if (action != null) {
        action.checkAndRun(params);
    }
}

到此,我们由用户“第一个有效”事件,单击弹出菜单,大致了解了FBReader是怎么去响应用户单击事件的了。而且也发现了诸如切换日夜间模式、设置阅读页面朝向、打开书籍目录、书籍书签等等一系列操作的定义,也就可以开始进行一些简单的设置处理了。

主题切换

myFBReaderApp.runAction(ActionCode.SWITCH_THEME_BLACK_PROFILE); SkinCompatManager.getInstance().loadSkin(ColorProfile.THEME_BLACK, SkinCompatManager.SKIN_LOADER_STRATEGY_BUILD_IN);

分享

myFBReaderApp.runAction(ActionCode.SHARE_BOOK);

添加书签

if (myFBReaderApp.getTextView().hasBookMark()) {
List<Bookmark> bookMarks = myFBReaderApp.getTextView().getBookMarks();
for (Bookmark bookmark : bookMarks) {
getCollection().deleteBookmark(bookmark);
}
} else {
getCollection().saveBookmark(myFBReaderApp.createBookmark(20, Bookmark.Type.BookMark));
}

章节切换

TOCTree aboveTOCElement = myFBReaderApp.getAboveTOCElement();
if (aboveTOCElement != null)
openBookText(aboveTOCElement);

切换页码

gotoPage(page);

亮度设置

myMainView.setScreenBrightness(progress);

字体大小

myFBReaderApp.runAction(ActionCode.DECREASE_FONT);

页码切换动画

PageTurningOptions pageTurningOptions = myFBReaderApp.PageTurningOptions;
pageTurningOptions.Animation.setValue(ZLView.Animation.curl);

重排

final ZLIntegerRangeOption spaceOption = myFBReaderApp.ViewOptions.getTextStyleCollection().getBaseStyle().LineSpaceOption;
spaceOption.setValue(15);
myFBReaderApp.clearTextCaches();
myFBReaderApp.getViewWidget().repaint();

字体大小

ZLTextBaseStyle baseStyle = myFBReaderApp.ViewOptions.getTextStyleCollection().getBaseStyle();
baseStyle.FontFamilyOption.setValue(fontText);
myFBReaderApp.clearTextCaches();
myFBReaderApp.getViewWidget().repaint();

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345