Android -- FBReader 阅读笔记 (一)

FBREADER 源码阅读笔记

前言

这篇文章是我在读源码时候的笔记。是我的一个习惯吧!在阅读源码的时候会记录一下思路,省得自己会忘记,相当于“保护现场”了吧。由于是一边看代码一边记录,一定会有很多的错误,请大家见谅。

一、代码导入

https://github.com/geometer/FBReaderJ 这个地址上就是fbreader的java项目。

版本库上面的项目是eclipse编写的,所以第一步,想办法把这个项目变成AS上开发的

(因为种种原因,我并没有clone github上的源码,在我们svn存在着之前的一个fbreader版本,是2.0的)

1、目的

导入fbreader这个项目是想在自己的程序中加入阅读器的功能。但从头开始开发时间长,并且没做过。所以参考了fbreader,在源码的基础上做二次开发。

代码同步下来之后,发现。fbreader并不是一个开源库(SDK),而是一个完整的项目,部分功能使用了jni开发,并且支持插件化,通过aidl,进行组件之间的通讯

因为要导入现有的项目(重构ing),想法是将fbreader整个项目编译成aar,然后导入现有项目。因为时间短,起初可以先把整个fbreader导入,之后对fbreader研究之后,或去掉相应模块或者重新开发

2、导入

导入过程比较繁琐,又没什么技术含量。主要就是将之前ant构建的项目换成gradle构建。

这里设计jni和aidl的目录结构有所变化,按照android studio上面的结构统一创建就好

(这里我犯了一个错误,fbreader的主项目千万别改报名,就按照之前的来,否则要改掉很多文件的import,相当费事儿。千万别改,千万别改,千万别改)

3、运行

先用ndk-build编出so, 然后在导入或者直接用android studio 带c++ 一起编, 都可以。反正 studio 也支持编译c语言了。

这里我直接使用ndk-build 编出so库,然后将so库添加到我的项目中去。省着clean项目的时候还要去重新编译,挺耗时间的。

二、源码目录

源码中的目录结构,其实我是在公司的svn里看到的,不知道谁写的,看时间,写这个文章的时候我刚上大学。

我就直接撸过来了。

用红笔画掉的是fbreader的一些三方以来,fbreader是主要的源码目录,

app 是我用来模仿公司的主项目的,其实就是一句startActivity。

以下是源码的一些目录

jni 的文件目录

整个项目的, 大概看一看

三、无目的的瞎看

网上的资源不是很多,项目也较老了。再加上从未接触过阅读相关的项目。扎铁了老心。没有头绪就从头看代码吧。

再 AndroidManifest 能知道应用的主activity是 org.geometerplus.android.fbreader.FBReader

(这里强插一句, 想运行fbreader这个项目,application是要继承自FBReaderApplication;还要修改FBReaderIntents类的第一行的包名,保持与项目的包名一致)

在FBReader先无目的看一下 FBReader::onCreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 捕获错误
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(this));

bindService(
new Intent(this, DataService.class),
DataConnection,
DataService.BIND_AUTO_CREATE
);

final Config config = Config.Instance();
config.runOnConnect(new Runnable() {
public void run() {
config.requestAllValuesForGroup("Options");
config.requestAllValuesForGroup("Style");
config.requestAllValuesForGroup("LookNFeel");
config.requestAllValuesForGroup("Fonts");
config.requestAllValuesForGroup("Colors");
config.requestAllValuesForGroup("Files");
}
});

final ZLAndroidLibrary zlibrary = getZLibrary();
myShowStatusBarFlag = zlibrary.ShowStatusBarOption.getValue();

requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.main);
myRootView = (RelativeLayout) findViewById(R.id.root_view);
myMainView = (ZLAndroidWidget) findViewById(R.id.main_view);
// setting keyboard default mode
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);

zlibrary.setActivity(this);

myFBReaderApp = (FBReaderApp) FBReaderApp.Instance();
if (myFBReaderApp == null) {
myFBReaderApp = new FBReaderApp(new BookCollectionShadow());
}
getCollection().bindToService(this, null);
myBook = null;

myFBReaderApp.setWindow(this);
myFBReaderApp.initWindow();

myFBReaderApp.setExternalFileOpener(new ExternalFileOpener(this));

getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
myShowStatusBarFlag ? 0 : WindowManager.LayoutParams.FLAG_FULLSCREEN
);

if (myFBReaderApp.getPopupById(TextSearchPopup.ID) == null) {
new TextSearchPopup(myFBReaderApp);
}
if (myFBReaderApp.getPopupById(NavigationPopup.ID) == null) {
new NavigationPopup(myFBReaderApp);
}
if (myFBReaderApp.getPopupById(SelectionPopup.ID) == null) {
new SelectionPopup(myFBReaderApp);
}

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));
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));

myFBReaderApp.addAction(ActionCode.PROCESS_HYPERLINK, new ProcessHyperlinkAction(this, myFBReaderApp));
myFBReaderApp.addAction(ActionCode.OPEN_VIDEO, new OpenVideoAction(this, myFBReaderApp));

myFBReaderApp.addAction(ActionCode.SHOW_CANCEL_MENU, new ShowCancelMenuAction(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 (ZLibrary.Instance().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));

final Intent intent = getIntent();
final String action = intent.getAction();

myOpenBookIntent = intent;
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
if (FBReaderIntents.Action.CLOSE.equals(action)) {
myCancelIntent = intent;
myOpenBookIntent = null;
} else if (FBReaderIntents.Action.PLUGIN_CRASH.equals(action)) {
myFBReaderApp.ExternalBook = null;
myOpenBookIntent = null;
getCollection().bindToService(this, new Runnable() {
public void run() {
myFBReaderApp.openBook(null, null, null);
}
});
}
}
}

onCreate的代码好长一堆,还什么都看不懂。最后一段貌似是openBook 的操作, 但是intent和action 都是空的,根本不执行FBReader也不存在父类,只能是在onResume()中了

1
FBReader::onResume()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@Override
protected void onResume() {
super.onResume();

SyncOperations.enableSync(this, true);

myStartTimer = true;
Config.Instance().runOnConnect(new Runnable() {
public void run() {
final int brightnessLevel =
getZLibrary().ScreenBrightnessLevelOption.getValue();
if (brightnessLevel != 0) {
setScreenBrightness(brightnessLevel);
} else {
setScreenBrightnessAuto();
}
if (getZLibrary().DisableButtonLightsOption.getValue()) {
setButtonLight(false);
}

getCollection().bindToService(FBReader.this, new Runnable() {
public void run() {
final BookModel model = myFBReaderApp.Model;
if (model == null || model.Book == null) {
return;
}
onPreferencesUpdate(myFBReaderApp.Collection.getBookById(model.Book.getId()));
}
});
}
});

registerReceiver(myBatteryInfoReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
IsPaused = false;
myResumeTimestamp = System.currentTimeMillis();
if (OnResumeAction != null) {
final Runnable action = OnResumeAction;
OnResumeAction = null;
action.run();
}

registerReceiver(mySyncUpdateReceiver, new IntentFilter(SyncOperations.UPDATED));

SetScreenOrientationAction.setOrientation(this, ZLibrary.Instance().getOrientationOption().getValue());

LogUtils.d("FBReader -> onResume cancelIntent: " + myCancelIntent);
LogUtils.d("FBReader -> onResume myOpenBookIntent: " + myOpenBookIntent);

if (myCancelIntent != null) {
final Intent intent = myCancelIntent;
myCancelIntent = null;
getCollection().bindToService(this, new Runnable() {
public void run() {
runCancelAction(intent);
}
});
return;
} else if (myOpenBookIntent != null) {
// it's maybe run here
final Intent intent = myOpenBookIntent;
myOpenBookIntent = null;
getCollection().bindToService(this, new Runnable() {
public void run() {
openBook(intent, null, true);
}
});
} else if (myFBReaderApp.getCurrentServerBook() != null) {
getCollection().bindToService(this, new Runnable() {
public void run() {
myFBReaderApp.useSyncInfo(true);
}
});
} else if (myFBReaderApp.Model == null && myFBReaderApp.ExternalBook != null) {
getCollection().bindToService(this, new Runnable() {
public void run() {
myFBReaderApp.openBook(myFBReaderApp.ExternalBook, null, null);
}
});
} else {
getCollection().bindToService(this, new Runnable() {
public void run() {
myFBReaderApp.useSyncInfo(true);
}
});
}

PopupPanel.restoreVisibilities(myFBReaderApp);
ApiServerImplementation.sendEvent(this, ApiListener.EVENT_READ_MODE_OPENED);
}

通过一顿的输出log并且debug代码,发现执行了onResume中最后的几个分支语句的第二个分支。

bindToService 这个是什么? 先不管它,往下看

接下来调用了

1
FBReader::openBook(Intent intent, final Runnable action, boolean force)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private synchronized void openBook(Intent intent, final Runnable action, boolean force) {
if (!force && myBook != null) {
return;
}

myBook = FBReaderIntents.getBookExtra(intent);
final Bookmark bookmark = FBReaderIntents.getBookmarkExtra(intent);
LogUtils.d("FBReader -> openBook myBook: " + myBook);
if (myBook == null) {
final Uri data = intent.getData();
LogUtils.d("FBReader -> openBook data: " + data);
if (data != null) {
myBook = createBookForFile(ZLFile.createFileByPath(data.getPath()));
}
}
if (myBook != null) {
ZLFile file = myBook.File;
LogUtils.d("FBReader -> openBook file path: " + file.getPath());
LogUtils.d("FBReader -> openBook file exists: " + file.exists());
if (!file.exists()) {
if (file.getPhysicalFile() != null) {
file = file.getPhysicalFile();
}
UIUtil.showErrorMessage(this, "fileNotFound", file.getPath());
myBook = null;
}
}


// 打开app 时 正常myBook为空 intent.getData 为空
/*
* 在主线程运行
*
* 正常打开时myBook, bookmark, action 三个参数都是空
* */
Config.Instance().runOnConnect(new Runnable() {
public void run() {
LogUtils.d("FBReader -> openBook run thread: " + Thread.currentThread());
LogUtils.d("FBReader -> openBook run myBook: " + myBook);
LogUtils.d("FBReader -> openBook run bookmark: " + bookmark);
LogUtils.d("FBReader -> openBook run action: " + action);
myFBReaderApp.openBook(myBook, bookmark, action);
AndroidFontUtil.clearFontCache();
}
});
}

直接能跟到最后几行的 myFBReaderApp.openBook(myBook, bookmark, action); 这一句

log输出,这三个参数都是空的。执行了FBReaderApp的openBook方法

1
FBReaderApp::(Book book, final Bookmark bookmark, Runnable postAction)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void openBook(Book book, final Bookmark bookmark, Runnable postAction) {
LogUtils.d("FBReaderApp -> openBook: " + Model);
if (Model != null) {
if (book == null || bookmark == null && book.File.equals(Model.Book.File)) {
return;
}
}

if (book == null) {
book = getCurrentServerBook();
if (book == null) {
showBookNotFoundMessage();
book = Collection.getRecentBook(0);
}
if (book == null || !book.File.exists()) {
// get helpfile
book = Collection.getBookByFile(BookUtil.getHelpFile());
}
if (book == null) {
return;
}
}
final Book bookToOpen = book;
bookToOpen.addLabel(Book.READ_LABEL);
Collection.saveBook(bookToOpen);

LogUtils.d("FBReaderApp -> openBook bookToOpen: " + bookToOpen);

final SynchronousExecutor executor = createExecutor("loadingBook");
executor.execute(new Runnable() {
public void run() {
openBookInternal(bookToOpen, bookmark, false);
}
}, postAction);
}

三个参数,大体上能猜测出是什么意思,但是,并不是很清晰。
执行到getCurrentServerBook一句时,但我们第一次启动应用是,此时的book对象是空,即使是getCurrentServerBook执行完之后还是空的。之后便去找到这个帮助文档getHelpFile。 然后转化成book对象。
之后,貌似创建了线程。

SynchronousExecutor这个东西是个接口由ZLApplication:: createExecutor(String key) 创建

1
ZLApplication:: createExecutor(String key)
1
2
3
4
5
6
7
protected SynchronousExecutor createExecutor(String key) {
if (myWindow != null) {
return myWindow.createExecutor(key);
} else {
return myDummyExecutor;
}
}

这里调用了myWindow的createExecutor方法,myWindow(ZLApplicationWindow)是 一个接口,FBReader实现了这个接口。
接着,调用了UIUtil的createExecutor方法

1
UIUtil::createExecutor(final Activity activity, final String key)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public static ZLApplication.SynchronousExecutor createExecutor(final Activity activity, final String key) {
return new ZLApplication.SynchronousExecutor() {
// 获得相应的文字资源
private final ZLResource myResource =
ZLResource.resource("dialog").getResource("waitMessage");
private final String myMessage = myResource.getResource(key).getValue();
private volatile ProgressDialog myProgress;

public void execute(final Runnable action, final Runnable uiPostAction) {
activity.runOnUiThread(new Runnable() {
public void run() {
// 在ui线程中创建一个对话框
myProgress = ProgressDialog.show(activity, null, myMessage, true, false);
// 在线程中执行第一个参数
final Thread runner = new Thread() {
public void run() {
// 在线程中运行第一个参数,也就是打开图书(在)
action.run();
// 执行完之后,关闭这个对话框
activity.runOnUiThread(new Runnable() {
public void run() {
try {
myProgress.dismiss();
myProgress = null;
} catch (Exception e) {
e.printStackTrace();
}
if (uiPostAction != null) {
uiPostAction.run();
}
}
});
}
};
runner.setPriority(Thread.MAX_PRIORITY);
runner.start();
}
});
}

private void setMessage(final ProgressDialog progress, final String message) {
if (progress == null) {
return;
}
activity.runOnUiThread(new Runnable() {
public void run() {
progress.setMessage(message);
}
});
}

public void executeAux(String key, Runnable runnable) {
setMessage(myProgress, myResource.getResource(key).getValue());
runnable.run();
setMessage(myProgress, myMessage);
}
};
}

主要看execute方法,这里先显示一个进度框,然后执行第一个参数action,然后关闭进度框,这个action 就是在 FBReaderApp::openBook(Book book, final Bookmark bookmark, Runnable postAction)中的

1
FBReaderApp::openBookInternal(bookToOpen, bookmark, false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

/**
* 打开内部的图书
*/
private synchronized void openBookInternal(Book book, Bookmark bookmark, boolean force) {
// 可能是跳转书签, 书签为空
LogUtils.d("FBReaderApp -> openBookInternal bookmark: " + bookmark);
if (!force && Model != null && book.equals(Model.Book)) {
if (bookmark != null) {
gotoBookmark(bookmark, false);
}
return;
}

onViewChanged();
storePosition();

BookTextView.setModel(null);
FootnoteView.setModel(null);
clearTextCaches();
Model = null;
ExternalBook = null;
System.gc();
System.gc();

// 猜测是根据book,加载一个用来读取这个book的插件
final FormatPlugin plugin = book.getPluginOrNull();
// 此时,阅读默认帮助文档时,插件为fb2
LogUtils.d("FBReaderApp -> openBookInternal plugin: " + plugin);
if (plugin instanceof ExternalFormatPlugin) {
ExternalBook = book;
final Bookmark bm;
if (bookmark != null) {
bm = bookmark;
} else {
ZLTextPosition pos = getStoredPosition(book);
if (pos == null) {
pos = new ZLTextFixedPosition(0, 0, 0);
}
bm = new Bookmark(book, "", pos, pos, "", false);
}
myExternalFileOpener.openFile((ExternalFormatPlugin) plugin, book, bm);
return;
}

try {
// 创建一个BookModel, 通过判断插件的type
Model = BookModel.createModel(book);
// BookCollectionShadow 暂时不懂
Collection.saveBook(book);
ZLTextHyphenator.Instance().load(book.getLanguage());
// 设置显示时的一些属性
BookTextView.setModel(Model.getTextModel());
setBookmarkHighlightings(BookTextView, null);
gotoStoredPosition();
if (bookmark == null) {
setView(BookTextView);
} else {
gotoBookmark(bookmark, false);
}
Collection.addBookToRecentList(book);
final StringBuilder title = new StringBuilder(book.getTitle());
if (!book.authors().isEmpty()) {
boolean first = true;
for (Author a : book.authors()) {
title.append(first ? " (" : ", ");
title.append(a.DisplayName);
first = false;
}
title.append(")");
}
setTitle(title.toString());
} catch (BookReadingException e) {
processException(e);
}

getViewWidget().reset();
getViewWidget().repaint();

try {
for (FileEncryptionInfo info : book.getPlugin().readEncryptionInfos(book)) {
if (info != null && !EncryptionMethod.isSupported(info.Method)) {
showErrorMessage("unsupportedEncryptionMethod", book.File.getPath());
break;
}
}
} catch (BookReadingException e) {
// ignore
}
}

目前能读懂的都在注释上,貌似在执行 setView(BookTextView)时,就会进行渲染的操作了

把帮助文档当成图书的话, 第一次出现对书的解析应该就是在BookUtil的getHelpFile的方法中

1
BookUtil::getHelpFile()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static ZLResourceFile getHelpFile() {
final Locale locale = Locale.getDefault();
// 获取local,取得帮助文档
ZLResourceFile file = ZLResourceFile.createResourceFile(
"data/help/MiniHelp." + locale.getLanguage() + "_" + locale.getCountry() + ".fb2"
);
if (file.exists()) {
return file;
}

file = ZLResourceFile.createResourceFile(
"data/help/MiniHelp." + locale.getLanguage() + ".fb2"
);
if (file.exists()) {
return file;
}

return ZLResourceFile.createResourceFile("data/help/MiniHelp.en.fb2");
}

通过固定的路径,调用了ZLResourceFile 的 createResourceFile

1
ZLResourceFile :: createResourceFile(String path)
1
2
3
4
5
6
7
8
public static ZLResourceFile createResourceFile(String path) {
ZLResourceFile file = ourCache.get(path);
if (file == null) {
file = ZLibrary.Instance().createResourceFile(path);
ourCache.put(path, file);
}
return file;
}

这里有个简单的缓存,然后调用了ZLibrary的createResourceFile方法。ZLibrary是个抽象类,ZLAndroidLibrary 实现了它, 并在application中进行了初始化操作。

1
ZLAndroidLibrary::createResourceFile(String path)
1
2
3
4
@Override
public ZLResourceFile createResourceFile(String path) {
return new AndroidAssetsFile(path);
}

这里,通过文件的路径,创建了一个AndroidAssetsFile。AndroidAssetsFile继承了ZLResourceFile,是ZLFile的子类。

ZLFile 是fbreader对所有文件的同意描述。上图是继承树。

以下是从网络上摘取的资料

  • ResourceFile类专门用来处理资源文件,这一章中要解析的assets文件夹下的资源文件都可以ZLResourceFile类来处理

  • ZLResourceFile类专门用来处理资源文件,这一章中要解析的assets文件夹下的资源文件都可以ZLResourceFile类来处理。

  • ZLPhysicalFile类专门用来处理普通文件,eoub文件就可以用一个ZLPhysicalFile类来代表。

  • ZLZipEntryFile类用来处理epub文件内部的xml文件,这个类会在第五章“epub文件处理 – 解压epub文件”中出现。

这三个文件类都实现了getInputStream抽象方法,不用的文件类会通过这个方法获得针对当前文件类的字节流类。

AndroidAssetsFile类(ZLResourceFile类的子类)的getInputStream方法会返回AssetInputStream类,这个类可以将资源文件转换成byte数组。

ZLPhysicalFile类的getInputStream方法会返回FileInputStream类,这个类可以将普通的文件转换成byte数组。

ZLZipEntryFile类的getInputStream方法会返回FileInputStream类,这个类可以将epub内部压缩过的xml文件转换成可以正常解析的byte数组

下面看一下AndroidAssetsFile的getInputStream方法。可以猜测,读取帮助文档的时候调用getInputStream会返回这个文件的InputStream

1
AndroidAssetsFile:: getInputStream()
1
2
3
4
 @Override
public InputStream getInputStream() throws IOException {
return myApplication.getAssets().open(getPath());
}

得到ZLResourceFile 对象之后,我们回到FBReaderApp::openBook 这个方法中。
可以看到通过

1
book = Collection.getBookByFile(BookUtil.getHelpFile());

将ZLResourceFile对象转成了Book 对象了。
Collection是一个接口IBookCollection, 这里是BookCollectionShadow实现了这个接口,在FBReaderApp的onCreate方法,我们可以看到这句

BookCollectionShadow又是什么呢?我们还要往下分析

上面的代码中创建了一个FBReaderApp对象, 至于这个对象是干什么的,现在还不知道。
接下来,getCollection 并 调用了 bindToService方法

1
FBReader::getCollection()
1
2
3
private BookCollectionShadow getCollection() {
return (BookCollectionShadow) myFBReaderApp.Collection;
}

调用的正是这个主activity的一个方法。返回的对象是FBReaderApp 的 Collection变量,这个正式刚才创建的BookCollectionShadow 实现了IBookCollection接口。

接着调用了BookCollectionShadow的bindToService方法

1
BookCollectionShadow::bindToService(Context context, Runnable onBindAction)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public synchronized void bindToService(Context context, Runnable onBindAction) {
if (myInterface != null && myContext == context) {
// not first connect
if (onBindAction != null) {
Config.Instance().runOnConnect(onBindAction);
}
} else {
// first connect
if (onBindAction != null) {
myOnBindActions.add(onBindAction);
}
context.bindService(
FBReaderIntents.internalIntent(FBReaderIntents.Action.LIBRARY_SERVICE),
this,
LibraryService.BIND_AUTO_CREATE
);
myContext = context;
}
}

这里执行了一句很熟悉的context.bindService方法,这里用到了aidl。这方面的问题就不记录了,跟主向无关。
bindService方法有三个参数,第二个参数传了BookCollectionShadow本身, 我们知道,bindService的第二个参数传的是ServiceConnection接口,在这里面, 我们可以调用aidl文件生命的方法,进行跨进程的通讯。也不知道fbreader为什么弄这么多进程是想干什么。

果然BookCollectionShadow实现了ServiceConnection,并且myInterface真是那个aidl的全局变量, 我们可以通过它,完成与服务端的沟通。


四、怎样获得book对象

经过一下午的瞎看,大致的熟悉了一下fbreader的源码。

带着问题学习总是最快的,那么,fbreader到底是怎样解析epub文件的呢。我们得找一个入口。在项目中,长按菜单键,会弹出一个功能列表,会看到一个本地书柜,一顿操作之后,我们可以找到一个我事先导入的一个电子书

最后我们会来到这个界面

在茫茫码海中怎么找到这个activity,用这样一条命令

1
adb shell dumpsys activity top | grep ACTIVITY --color

在这个activitiy中, mybook是通过intent传入的,所以找上个页面

1
2
3
4

![](http://upload-images.jianshu.io/upload_images/1285832-748d5e53cba5441a.png)

ListActivity是什么? 没用过,TreeActivity自己写的, 太复杂。想着在LibraryActivity中能找到一些方法

LibraryActivity::onListItemClick(ListView listView, View view, int position, long rowId)

1
2
3
4
5
6
7
8
9
10
11
12
```
@Override
protected void onListItemClick(ListView listView, View view, int position, long rowId) {
final LibraryTree tree = (LibraryTree)getListAdapter().getItem(position);
final Book book = tree.getBook();
LogUtils.d("LibraryActivity -> onListItemClick book: " + book);
if (book != null) {
showBookInfo(book);
} else {
openTree(tree);
}
}

在这个方法中, 是通过

1
2
3
4
```
public Book getBook() {
return null;
}

这尼玛返回空!!!
tree是LibraryTree的一个实例,在TreeAdapter的getItem(int position) 方法中得到的

1
2
3
public FBTree getItem(int position) {
return myItems.get(position);
}

存放在了叫

final List myItems;```这样的一个list之中。
1
2
3
4
5
6
7
8
9
10
11
12
13

一路尾随,不是跟踪myItems, 看一下TreeAdapter的replaceAll方法

![](http://upload-images.jianshu.io/upload_images/1285832-34f72c5912915c2c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

根据名字我们能判断。得到现在树的子树,然后添加到myItem中去的。那么我们知道现在的树, 或者现在树的子树是什么,应该就可以得到答案了。

现在的树集成关系是这样的
![](http://upload-images.jianshu.io/upload_images/1285832-5bb780b3dd24cca2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

又在onListItemClick方法中强装成LibraryTree, 我们只需关注LibraryTree的之类就行了

这个树通过getTreeByKey得到

LibraryActivity::getTreeByKey(FBTree.Key key)

1
2
3
4
5
6
```
@Override
protected LibraryTree getTreeByKey(FBTree.Key key) {
return key != null ? myRootTree.getLibraryTree(key) : myRootTree;
}
private synchronized void deleteRootTree() {

最后我们要找的这棵树实际跟

1
myRootTree在onCreate的时候创建

LibraryActivity::onCreate(Bundle icicle)

1
2


@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
Log.d(TAG, “start onCreate function: “);
mySelectedBook = FBReaderIntents.getBookExtra(getIntent());
new LibraryTreeAdapter(this);
getListView().setTextFilterEnabled(true);
getListView().setOnCreateContextMenuListener(this);
deleteRootTree();
myCollection.bindToService(this, new Runnable() {
public void run() {
setProgressBarIndeterminateVisibility(!myCollection.status().IsCompleted);
myRootTree = new RootTree(myCollection);
myCollection.addListener(LibraryActivity.this);
init(getIntent());
}
});
}

1
2
3
4
5
6
在倒数第二行add的接口的回掉在这里

![](http://upload-images.jianshu.io/upload_images/1285832-60b8eb000aceeeb5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
在```onBookEvent中一定是这个树的来源```,看来我们要去另一个进程里看看了

这个服务和之前的一样LibraryService。我们跟进LibraryService

@Override
public IBinder onBind(Intent intent) {
return myLibrary;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
返回的真是aidl的一个接口

![](http://upload-images.jianshu.io/upload_images/1285832-89620d920299568d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

以上是接口创建的一部分代码

![](http://upload-images.jianshu.io/upload_images/1285832-1c0cd3f0f572740c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

在reset中搞了一个Config.Instance().runOnConnect不知道是干嘛用的,反正是调用了以下的方法

![](http://upload-images.jianshu.io/upload_images/1285832-a28e8fafc6062a50.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我注意到底下的两个广播,很明显是做进程间通讯。然后, 我在BookCollectionShadow中找到了这个广播的接收者

private final BroadcastReceiver myReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
if (!hasListeners()) {
return;
}
try {
final String type = intent.getStringExtra(“type”);
LogUtils.d(“BookCollectionShadow -> onReceive type: “ + type);
if (LibraryService.BOOK_EVENT_ACTION.equals(intent.getAction())) {
final Book book = SerializerUtil.deserializeBook(intent.getStringExtra(“book”));
fireBookEvent(BookEvent.valueOf(type), book);
} else {
fireBuildEvent(Status.valueOf(type));
}
} catch (Exception e) {
// ignore
}
}
};

1
2
3
4
5
6
7
8
接下来看log

![](http://upload-images.jianshu.io/upload_images/1285832-06ada923f3db31b6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
通过这个<entry xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata"> 搞成一本书???

这个先别管,我的疑惑是tree.getBook() 怎么返回空?

经过上面的弯路, 我们知道了最后回掉的是```LibraryActivity:: onBookEvent(BookEvent event, Book book)```这个方法

@Override
public void onBookEvent(BookEvent event, Book book) {
if (getCurrentTree().onBookEvent(event, book)) {
getListAdapter().replaceAll(getCurrentTree().subtrees(), true);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

再跟一下里面的方法

```java
public boolean onBookEvent(BookEvent event, Book book) {
switch (event) {
default:
case Added:
return false;
case Removed:
return removeBook(book);
case Updated:
{
boolean changed = false;
for (FBTree tree : this) {
if (tree instanceof BookTree) {
final Book b = ((BookTree)tree).Book;
if (b.equals(book)) {
b.updateFrom(book);
changed = true;
}
}
}
return changed;
}
}
}

我们看到了booktree这个东西,原来我们正经使用的是booktree的getBook,所以get得到的肯定是一本书

这里留一个问题, 这个booktree是怎么来的? 先不着急分析他

我们发现book 是从booktree 得到的, 而booktree中的book 是构造是传进来的。

而这些又是在FilteredTree的抽象发方法createSubtree, 得到的

接着往上看FilteredTree这个类

终于找到主进程里的book了, 原来是调用进程里的 Collection.books(query);方法, 来获得一个book的列表。

五、怎么跳转到Fbreader这个activity

本来想看看怎么解析epub的,但是感觉目前还消化不了。

我们要把这个完整的项目当成一个sdk来使用,虽然说整个加到工程中fbreader的5M左右了,但是没有办法,时间紧任务重。我倒是很赞同自己去写个阅读器,但是条件不允许。

我们要使用这个项目, 就得找到一个书, 然后跳转到这个activity让他显示。

经过以上的分析, 可以看到,在查找书的时候 已经就装成book对象了。反正要是我写,我肯定在查找文件的时候返回的url, 然后根据这个去解析成book的对象。

记得最早之前分析过,在FBReader的onResume是启动的关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private synchronized void openBook(Intent intent, final Runnable action, boolean force) {
if (!force && myBook != null) {
return;
}

myBook = FBReaderIntents.getBookExtra(intent);
final Bookmark bookmark = FBReaderIntents.getBookmarkExtra(intent);
LogUtils.d("FBReader -> openBook myBook: " + myBook);
if (myBook == null) {
final Uri data = intent.getData();
LogUtils.d("FBReader -> openBook data: " + data);
if (data != null) {
myBook = createBookForFile(ZLFile.createFileByPath(data.getPath()));
}
}
if (myBook != null) {
ZLFile file = myBook.File;
LogUtils.d("FBReader -> openBook file path: " + file.getPath());
LogUtils.d("FBReader -> openBook file exists: " + file.exists());
if (!file.exists()) {
if (file.getPhysicalFile() != null) {
file = file.getPhysicalFile();
}
UIUtil.showErrorMessage(this, "fileNotFound", file.getPath());
myBook = null;
}
}

这里,但书为空的时时候,在intent.getData去 拿到url, 传到ZLFile里去。也就是说,我在start这个activity 传一个url 进去, 于是我这样写了一段

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 打开一本电子书
*
* @param context 上下文
* @param path epub 资源的绝对路径
*/
public static void openBookActivity(Context context, String path) {
final Intent intent = new Intent(context, FBReader.class)
.setAction(FBReaderIntents.Action.VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.setData(Uri.parse(path));
context.startActivity(intent);
}

这样就可以了。 我翻了几页之后, 默认就会保存阅读的位置。那么我想重新阅读这个文章应该怎么办。继续撸代码

book, Bookmark bookmark, boolean force)```方法中,第一次打开书是会执行```gotoStoredPosition();```这样一个方法,从字面的意思是去到存储的位置
1
2

具体的方法是这样的

FBReaderApp::gotoStoredPosition()

1
2
3
4
5
6
7
8
9
10
```
private void gotoStoredPosition() {
myStoredPositionBook = Model != null ? Model.Book : null;
if (myStoredPositionBook == null) {
return;
}
myStoredPosition = getStoredPosition(myStoredPositionBook);
BookTextView.gotoPosition(myStoredPosition);
savePosition();
}

是调用了BookTextView的gotoPosition这个方法。那我们看看能否在FBReader这个类上搞点事情。在openBook方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private synchronized void openBook(Intent intent, final Runnable action, boolean force) {
if (!force && myBook != null) {
return;
}

myBook = FBReaderIntents.getBookExtra(intent);
final Bookmark bookmark = FBReaderIntents.getBookmarkExtra(intent);
LogUtils.d("FBReader -> openBook myBook: " + myBook);
if (myBook == null) {
final Uri data = intent.getData();
LogUtils.d("FBReader -> openBook data: " + data);
if (data != null) {
myBook = createBookForFile(ZLFile.createFileByPath(data.getPath()));
}
}

LogUtils.d("FBReader -> openBook mybook: " + myBook);
if (myBook != null) {
ZLFile file = myBook.File;
LogUtils.d("FBReader -> openBook file path: " + file.getPath());
LogUtils.d("FBReader -> openBook file exists: " + file.exists());
if (!file.exists()) {
if (file.getPhysicalFile() != null) {
file = file.getPhysicalFile();
}
UIUtil.showErrorMessage(this, "fileNotFound", file.getPath());
myBook = null;
}
}


// 打开app 时 正常myBook为空 intent.getData 为空
/*
* 在主线程运行
*
* 正常打开时myBook, bookmark, action 三个参数都是空
* */
Config.Instance().runOnConnect(new Runnable() {
public void run() {
LogUtils.d("FBReader -> openBook run thread: " + Thread.currentThread());
LogUtils.d("FBReader -> openBook run myBook: " + myBook);
LogUtils.d("FBReader -> openBook run bookmark: " + bookmark);
LogUtils.d("FBReader -> openBook run action: " + action);
myFBReaderApp.openBook(myBook, bookmark, action);
// // TODO: 6/7/2017 回到首页
myFBReaderApp.BookTextView.gotoHome();
}
});
}

在最后一句,这个activity中可以拿到myFBReaderApp,猜想是用来控制整个阅读器的类, 调用gotohome, 竟然可以了。通过这个就可以控制是否继续阅读还是从头开始

六、跳转到固定章节

一本书有很多章节,跳转到固定章节的时候不可能进行一步步的翻页操作。碰巧fbreader提供这样的功能,而且还有快速翻看

找打开一本书之后Model 字段就会赋值, ‘弹幕’一下这个字段, 看到里面确实存在了章节的信息

Paste_Image.png

我知道了章节,然后怎么去跳转,我们看下面这一段代码:

1
TOCActivity::openBookText(TOCTree tree)

1
2
3
4
5
6
7
8
9
10
11
void openBookText(TOCTree tree) {
final TOCTree.Reference reference = tree.getReference();
if (reference != null) {
finish();
final FBReaderApp fbreader = (FBReaderApp)ZLApplication.Instance();
fbreader.addInvisibleBookmark();
fbreader.BookTextView.gotoPosition(reference.ParagraphIndex, 0, 0);
fbreader.showBookTextView();
fbreader.storePosition();
}
}

得到reference对象的ParagraphIndex, 然后去调用BookTextView的gotoPosition

在FBReaderApp中这样写试试

这样是可以达到预期效果的,但是有一定要值得注意:
在第一次读取书时,获取书的操作是个异步的, 也就是说,这个时候Model可能为空,所以在以后开发中,最好是用接口,将获取书的情况反到activity中, 这样,当书加载完成时再去做相应的跳转操作。

七、字体加大与缩小

源码中,改变字体大小的就在菜单的按键中

点击按键监听再这里,这么搞也是特殊,从未见过啊,不知道干嘛弄的这么复杂。像这样可以实现这个功能了。

这样只是增加与减少,万一需求上是给定几个固定的字号,然后调节怎么办?所以开始得看看源码

跟进去发现,最终的action是存在于这个map, 实在主activity创建时put的,所以所有的操作都会交给ZLAction的子类去处理


也就是上面选中的这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ChangeFontSizeAction extends FBAction {
private final int myDelta;

ChangeFontSizeAction(FBReaderApp fbreader, int delta) {
super(fbreader);
myDelta = delta;
}

@Override
protected void run(Object ... params) {
final ZLIntegerRangeOption option =
Reader.ViewOptions.getTextStyleCollection().getBaseStyle().FontSizeOption;
option.setValue(option.getValue() + myDelta);

LogUtils.d("ChangeFontSizeAction -> run: " + option.getValue());
Reader.clearTextCaches();
Reader.getViewWidget().repaint();
}
}

执行run方法后会设置字号,这里的option.setValue(option.getValue() + myDelta);就是对字号的设置。要是愿意的话可以复写FBAction或者直接使用run方法中的参数进行传值,当然,后一种要好一点。

八、音量键功能

一个功能完整的阅读器,音量键也都会派上用场。fbreader音量键也不例外

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ZLAndroidWidget::@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
final ZLApplication application = ZLApplication.Instance();
final ZLKeyBindings bindings = application.keyBindings();

if (bindings.hasBinding(keyCode, true)
|| bindings.hasBinding(keyCode, false)) {
if (myKeyUnderTracking != -1) {
if (myKeyUnderTracking == keyCode) {
return true;
} else {
myKeyUnderTracking = -1;
}
}
if (bindings.hasBinding(keyCode, true)) {
myKeyUnderTracking = keyCode;
myTrackingStartTime = System.currentTimeMillis();
return true;
} else {
return application.runActionByKey(keyCode, false);
}
} else {
return false;
}
}
1
2
3
4
5
6
7
8
public final boolean runActionByKey(int key, boolean longPress) {
final String actionId = keyBindings().getBinding(key, longPress);
if (actionId != null) {
final ZLAction action = myIdToActionMap.get(actionId);
return action != null && action.checkAndRun();
}
return false;
}

程序在runActionByKey方法中控制这按键

key 是当前案件的键码,会对所有按键处理,如果你接键盘的话。
longPress 字面意思是是否长按,但是我试过永远的短按,永远的false

以后处理案件就可在这里处理,或者深入到action里面进行处理。

九、更换背景,字体颜色

更换背景以及字体颜色,也是实现夜间模式的一个套路。首先我们定位到PreferenceActivity,冷不丁一看,我去,这不是系统里的settings嘛。这么长的init至于么?

这么长的代码就不全粘贴了,大概在400多行,有这么一段。是添加背景和墙纸的
我们跟到这个类中BackgroundPreference

在onBindView下面是跳转到颜色选择器的代码,在PreferenceActivity中的onActivityResult方法中返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (myNetworkContext.onActivityResult(requestCode, resultCode, data)) {
return;
}
if (resultCode != RESULT_OK) {
return;
}
if (BACKGROUND_REQUEST_CODE == requestCode) {
if (myBackgroundPreference != null) {
myBackgroundPreference.update(data);
}
return;
}
myChooserCollection.update(requestCode, data);
}

中间部分, 调用了yBackgroundPreference.update(data);

1
2
3
4
5
6
7
8
9
10
11
12
13
public void update(Intent data) {
final String value = data.getStringExtra(VALUE_KEY);
LogUtils.d("BackgroundPreference -> update: " + value);
if (value != null) {
myProfile.WallpaperOption.setValue(value);
}
final int color = data.getIntExtra(COLOR_KEY, -1);
LogUtils.d("BackgroundPreference -> update: " + color);
if (color != -1) {
myProfile.BackgroundOption.setValue(new ZLColor(color));
}
notifyChanged();
}

继续往里跟,方向设置颜色或者是壁纸图片, 只是设置了WallpaperOption的value。我们能在ColorProfile类中找到这些参数,那么我们怎么在主activity获取并且改变它呢

还是最重要的

的实例 ViewOptions,通过它我们就能拿到ColorProfile, 图下设置背景为红色
1
2
```
myFBReaderApp.ViewOptions.getColorProfile().BackgroundOption.setValue(new ZLColor(255, 0, 0));

但是直接这么写,没有变化,仔细想想。我只设置了颜色,但是没有通知重绘,自然就没有变化。

那么,颜色选择怎么通知到这个activity的呢,只能是Intent传的,我们看下FBReader 的onActivityResult(int requestCode, int resultCode, Intent data)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_PREFERENCES:
if (resultCode != RESULT_DO_NOTHING && data != null) {
final Book book = FBReaderIntents.getBookExtra(data);
if (book != null) {
getCollection().bindToService(this, new Runnable() {
public void run() {
onPreferencesUpdate(book);
}
});
}
}
break;
case REQUEST_CANCEL_MENU:
runCancelAction(data);
break;
}
}

ok, 确实有我需要的东西,这个方法调用了onPreferencesUpdate

1
2
3
4
private void onPreferencesUpdate(Book book) {
AndroidFontUtil.clearFontCache();
myFBReaderApp.onBookUpdated(book);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
\    public void onBookUpdated(Book book) {
if (Model == null || Model.Book == null || !Model.Book.equals(book)) {
return;
}

final String newEncoding = book.getEncodingNoDetection();
final String oldEncoding = Model.Book.getEncodingNoDetection();

Model.Book.updateFrom(book);

if (newEncoding != null && !newEncoding.equals(oldEncoding)) {
reloadBook();
} else {
ZLTextHyphenator.Instance().load(Model.Book.getLanguage());
clearTextCaches();
getViewWidget().repaint();
}
}

起到决定性作用的就行最后一句 getViewWidget().repaint();

那么,更改背景颜色就是
(上面的代码都是等书加载完毕之后,显示在view中在去设置的, 不然书都没加载完,自然也就设置不了)


BackgroundOption 是背景色
RegularTextOption 是文字的颜色

十、动画类型

下面开始研究应用的翻页动画。
我们修改颜色实际上是修改了ZLAndroidWidget。我们可以跟进这个类看一下代码

1
ZLAndroidWidget::getAnimationProvider()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private AnimationProvider getAnimationProvider() {
final ZLView.Animation type = ZLApplication.Instance().getCurrentView()
.getAnimationType();
if (myAnimationProvider == null || myAnimationType != type) {
myAnimationType = type;
switch (type) {
case none:
myAnimationProvider = new NoneAnimationProvider(myBitmapManager);
break;
case curl:
myAnimationProvider = new CurlAnimationProvider(myBitmapManager);
break;
case slide:
myAnimationProvider = new SlideAnimationProvider(
myBitmapManager);
break;
case shift:
myAnimationProvider = new ShiftAnimationProvider(
myBitmapManager);
break;
case left2right:
myAnimationProvider = new Left2RightAnimationProvider(
myBitmapManager);
break;
case simulation:
// myAnimationProvider = new SimulateAnimationProvider(
// myBitmapManager);
myAnimationProvider = new EmulateAnimationProvider(
myBitmapManager);
break;
}
}
return myAnimationProvider;
}

代码跟你的不一样,正常,这个我改过了。
在绘制动画的时候,也就是onDrawInScrolling(Canvas canvas)这个方法被调用的时候,都会获取一下当前的动画。

那么在设置动画的时候调用的是PreferenceActivity这个activity,再init一堆东西里看以看到,这样一句话

原来是改变的是pageTurningOptions这个东西,我们再看主activity中能不能找到这个对象。回到FBReader当中。我们可以这样设置动画

1
myFBReaderApp.PageTurningOptions.Animation.setValue(ZLView.Animation.left2right);

因为是执行每一次翻页的动作都会get一下当前的动画,所以也就不需要重绘当前页面,写这样一句就好。

十一、点击区域

市场上的阅读类应用。基本都是点击左面上一页,右面下一页。中间会弹出一个设置菜单。
我发现我的这个version的代码,点击中间是没有任何反应的,所以。继续撸码。

关于点击时间, 首先去看ZLAndroidWidget的onTouchEvent方法

1
ZLAndroidWidget::onTouchEvent(MotionEvent event)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
final ZLView view = ZLApplication.Instance().getCurrentView();
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
if (myPendingDoubleTap) {
view.onFingerDoubleTap(x, y);
} else if (myLongClickPerformed) {
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 {
view.onFingerSingleTap(x, y);
}
} else {
view.onFingerRelease(x, y);
}
}
myPendingDoubleTap = false;
myPendingPress = false;
myScreenIsTouched = false;
break;
case MotionEvent.ACTION_DOWN:
if (myPendingShortClickRunnable != null) {
removeCallbacks(myPendingShortClickRunnable);
myPendingShortClickRunnable = null;
myPendingDoubleTap = true;
} else {
postLongClickRunnable();
myPendingPress = true;
}
myScreenIsTouched = true;
myPressedX = x;
myPressedY = y;
break;
case MotionEvent.ACTION_MOVE: {
final int slop = ViewConfiguration.get(getContext())
.getScaledTouchSlop();
final boolean isAMove = Math.abs(myPressedX - x) > slop
|| Math.abs(myPressedY - y) > slop;
if (isAMove) {
myPendingDoubleTap = false;
}
if (myLongClickPerformed) {
view.onFingerMoveAfterLongPress(x, y);
} else {
if (myPendingPress) {
if (isAMove) {
if (myPendingShortClickRunnable != null) {
removeCallbacks(myPendingShortClickRunnable);
myPendingShortClickRunnable = null;
}
if (myPendingLongClickRunnable != null) {
removeCallbacks(myPendingLongClickRunnable);
}
view.onFingerPress(myPressedX, myPressedY);
myPendingPress = false;
}
}
if (!myPendingPress) {
view.onFingerMove(x, y);
}
}
break;
}
}
return true;
}

当按键抬起的时候, 将手指的位置传给了 view.onFingerSingleTap(x, y);
view就是FBView这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public boolean onFingerSingleTap(int x, int y) {
if (super.onFingerSingleTap(x, y)) {
return true;
}
final ZLTextRegion hyperlinkRegion = findRegion(x, y, MAX_SELECTION_DISTANCE, ZLTextRegion.HyperlinkFilter);
if (hyperlinkRegion != null) {
// click link
selectRegion(hyperlinkRegion);
myReader.getViewWidget().reset();
myReader.getViewWidget().repaint();
myReader.runAction(ActionCode.PROCESS_HYPERLINK);
return true;
}
final ZLTextRegion videoRegion = findRegion(x, y, 0, ZLTextRegion.VideoFilter);
if (videoRegion != null) {
// click video
selectRegion(videoRegion);
myReader.getViewWidget().reset();
myReader.getViewWidget().repaint();
myReader.runAction(ActionCode.OPEN_VIDEO, (ZLTextVideoRegionSoul) videoRegion.getSoul());
return true;
}
final ZLTextHighlighting highlighting = findHighlighting(x, y, MAX_SELECTION_DISTANCE);
if (highlighting instanceof BookmarkHighlighting) {
myReader.runAction(
ActionCode.SELECTION_BOOKMARK,
((BookmarkHighlighting) highlighting).Bookmark
);
return true;
}
String actionId = getZoneMap()
.getActionByCoordinates(x, y, getContextWidth(), getContextHeight(),
isDoubleTapSupported() ? TapZoneMap.Tap.singleNotDoubleTap : TapZoneMap.Tap.singleTap);
myReader.runAction(actionId, x, y);
return true;
}

这段的最后一句就是或坐标的区域

一路跟下去, 发现

这样一个类,看它的构造
1
2
3
4
5
6
7
8
9
10
11
12
13
```
private TapZoneMap(String name) {
Name = name;
myOptionGroupName = "TapZones:" + name;

LogUtils.d("TapZoneMap -> TapZoneMap: " + name);
myHeight = new ZLIntegerRangeOption(myOptionGroupName, "Height", 2, 5, 3);
myWidth = new ZLIntegerRangeOption(myOptionGroupName, "Width", 2, 5, 3);
final ZLFile mapFile = ZLFile.createFileByPath(
"default/tapzones/" + name.toLowerCase() + ".xml"
);
new Reader().readQuietly(mapFile);
}

是读了一个文件,我们在资源文件中找下


果然在这里躺着一堆的文件,应该是按照屏幕的方向 选择默认的配置文件,这里默认的就是right_to_left.xml,我们打开看看。

1
2
3
4
5
6
7
8
9
10
11
<tapZones v="3" h="3">
<zone x="0" y="0" action="previousPage" action2="navigate"/>
<zone x="0" y="1" action="previousPage"/>
<zone x="0" y="2" action="previousPage" action2="menu"/>
<zone x="1" y="0" action2="navigate"/>
<zone x="1" y="1" action2="menu"/>
<zone x="1" y="2" action2="menu"/>
<zone x="2" y="0" action="nextPage" action2="navigate"/>
<zone x="2" y="1" action="nextPage"/>
<zone x="2" y="2" action="nextPage" action2="menu"/>
</tapZones>

猜测一下,fbreader应该是把屏幕分成3 x 3的区域,大概就是上面图片的意思。用这个坐标代表,我要点击屏幕中间的,自然我就加了一个1,1的坐标。

得到区域后,执行了

myReader.runAction(actionId, x, y);``` 这个方法。
1
跟音量键的一样,他会把事件分发的```ShowMenuAction```这个类里面。

class ShowMenuAction extends FBAndroidAction {
ShowMenuAction(FBReader baseActivity, FBReaderApp fbreader) {
super(baseActivity, fbreader);
}

@Override
protected void run(Object ... params) {
    BaseActivity.openOptionsMenu();
    //BaseActivity.menu();
}

}
`
源码里调用的是openOptionsMenu,这个baseactivity就是我们的FBReader这个类。我改成自己的方法, 就可以定制自己的菜单栏了,然后抛弃源码提供的菜单栏。

ps:代码看到这里差不多可以进行定制了, 但是,代码里确实是有些无用的东西, 要是能把这些东西去掉的话,做一下精简。应该会更好。