FBREADER 源碼閱讀筆記
前言
這篇文章是我在讀源碼時(shí)候的筆記。是我的一個(gè)習(xí)慣吧!在閱讀源碼的時(shí)候會記錄一下思路,省得自己會忘記,相當(dāng)于“保護(hù)現(xiàn)場”了吧。由于是一邊看代碼一邊記錄,一定會有很多的錯(cuò)誤,請大家見諒。
一、代碼導(dǎo)入
在 https://github.com/geometer/FBReaderJ 這個(gè)地址上就是fbreader的java項(xiàng)目。
版本庫上面的項(xiàng)目是eclipse編寫的,所以第一步,想辦法把這個(gè)項(xiàng)目變成AS上開發(fā)的
(因?yàn)榉N種原因,我并沒有clone github上的源碼,在我們svn存在著之前的一個(gè)fbreader版本,是2.0的)
1、目的
導(dǎo)入fbreader這個(gè)項(xiàng)目是想在自己的程序中加入閱讀器的功能。但從頭開始開發(fā)時(shí)間長,并且沒做過。所以參考了fbreader,在源碼的基礎(chǔ)上做二次開發(fā)。
代碼同步下來之后,發(fā)現(xiàn)。fbreader并不是一個(gè)開源庫(SDK),而是一個(gè)完整的項(xiàng)目,部分功能使用了jni開發(fā),并且支持插件化,通過aidl,進(jìn)行組件之間的通訊
因?yàn)橐獙?dǎo)入現(xiàn)有的項(xiàng)目(重構(gòu)ing),想法是將fbreader整個(gè)項(xiàng)目編譯成aar,然后導(dǎo)入現(xiàn)有項(xiàng)目。因?yàn)闀r(shí)間短,起初可以先把整個(gè)fbreader導(dǎo)入,之后對fbreader研究之后,或去掉相應(yīng)模塊或者重新開發(fā)
2、導(dǎo)入
導(dǎo)入過程比較繁瑣,又沒什么技術(shù)含量。主要就是將之前ant構(gòu)建的項(xiàng)目換成gradle構(gòu)建。
這里設(shè)計(jì)jni和aidl的目錄結(jié)構(gòu)有所變化,按照android studio上面的結(jié)構(gòu)統(tǒng)一創(chuàng)建就好
(這里我犯了一個(gè)錯(cuò)誤,fbreader的主項(xiàng)目千萬別改報(bào)名,就按照之前的來,否則要改掉很多文件的import,相當(dāng)費(fèi)事兒。千萬別改,千萬別改,千萬別改)
3、運(yùn)行
先用ndk-build編出so, 然后在導(dǎo)入或者直接用android studio 帶c++ 一起編, 都可以。反正 studio 也支持編譯c語言了。
這里我直接使用ndk-build 編出so庫,然后將so庫添加到我的項(xiàng)目中去。省著clean項(xiàng)目的時(shí)候還要去重新編譯,挺耗時(shí)間的。
二、源碼目錄
源碼中的目錄結(jié)構(gòu),其實(shí)我是在公司的svn里看到的,不知道誰寫的,看時(shí)間,寫這個(gè)文章的時(shí)候我剛上大學(xué)。
我就直接擼過來了。

用紅筆畫掉的是fbreader的一些三方以來,fbreader是主要的源碼目錄,
app 是我用來模仿公司的主項(xiàng)目的,其實(shí)就是一句startActivity。
以下是源碼的一些目錄

jni 的文件目錄

整個(gè)項(xiàng)目的, 大概看一看

三、無目的的瞎看
網(wǎng)上的資源不是很多,項(xiàng)目也較老了。再加上從未接觸過閱讀相關(guān)的項(xiàng)目。扎鐵了老心。沒有頭緒就從頭看代碼吧。
再 AndroidManifest 能知道應(yīng)用的主activity是org.geometerplus.android.fbreader.FBReader
(這里強(qiáng)插一句, 想運(yùn)行fbreader這個(gè)項(xiàng)目,application是要繼承自FBReaderApplication;還要修改FBReaderIntents類的第一行的包名,保持與項(xiàng)目的包名一致)
在FBReader先無目的看一下
FBReader::onCreate
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 捕獲錯(cuò)誤
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 都是空的,根本不執(zhí)行FBReader也不存在父類,只能是在onResume()中了
FBReader::onResume()
@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代碼,發(fā)現(xiàn)執(zhí)行了onResume中最后的幾個(gè)分支語句的第二個(gè)分支。
bindToService 這個(gè)是什么? 先不管它,往下看
接下來調(diào)用了
FBReader::openBook(Intent intent, final Runnable action, boolean force)
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 時(shí) 正常myBook為空 intent.getData 為空
/*
* 在主線程運(yùn)行
*
* 正常打開時(shí)myBook, bookmark, action 三個(gè)參數(shù)都是空
* */
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輸出,這三個(gè)參數(shù)都是空的。執(zhí)行了FBReaderApp的openBook方法
FBReaderApp::(Book book, final Bookmark bookmark, Runnable postAction)
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);
}
三個(gè)參數(shù),大體上能猜測出是什么意思,但是,并不是很清晰。
執(zhí)行到getCurrentServerBook一句時(shí),但我們第一次啟動應(yīng)用是,此時(shí)的book對象是空,即使是getCurrentServerBook執(zhí)行完之后還是空的。之后便去找到這個(gè)幫助文檔getHelpFile。 然后轉(zhuǎn)化成book對象。
之后,貌似創(chuàng)建了線程。
SynchronousExecutor這個(gè)東西是個(gè)接口由ZLApplication:: createExecutor(String key) 創(chuàng)建
ZLApplication:: createExecutor(String key)
protected SynchronousExecutor createExecutor(String key) {
if (myWindow != null) {
return myWindow.createExecutor(key);
} else {
return myDummyExecutor;
}
}
這里調(diào)用了myWindow的createExecutor方法,myWindow(ZLApplicationWindow)是 一個(gè)接口,F(xiàn)BReader實(shí)現(xiàn)了這個(gè)接口。
接著,調(diào)用了UIUtil的createExecutor方法
UIUtil::createExecutor(final Activity activity, final String key)
public static ZLApplication.SynchronousExecutor createExecutor(final Activity activity, final String key) {
return new ZLApplication.SynchronousExecutor() {
// 獲得相應(yīng)的文字資源
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線程中創(chuàng)建一個(gè)對話框
myProgress = ProgressDialog.show(activity, null, myMessage, true, false);
// 在線程中執(zhí)行第一個(gè)參數(shù)
final Thread runner = new Thread() {
public void run() {
// 在線程中運(yùn)行第一個(gè)參數(shù),也就是打開圖書(在)
action.run();
// 執(zhí)行完之后,關(guān)閉這個(gè)對話框
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方法,這里先顯示一個(gè)進(jìn)度框,然后執(zhí)行第一個(gè)參數(shù)action,然后關(guān)閉進(jìn)度框,這個(gè)action 就是在 FBReaderApp::openBook(Book book, final Bookmark bookmark, Runnable postAction)中的
FBReaderApp::openBookInternal(bookToOpen, bookmark, false);
/**
* 打開內(nèi)部的圖書
*/
private synchronized void openBookInternal(Book book, Bookmark bookmark, boolean force) {
// 可能是跳轉(zhuǎn)書簽, 書簽為空
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();
// 猜測是根據(jù)book,加載一個(gè)用來讀取這個(gè)book的插件
final FormatPlugin plugin = book.getPluginOrNull();
// 此時(shí),閱讀默認(rèn)幫助文檔時(shí),插件為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 {
// 創(chuàng)建一個(gè)BookModel, 通過判斷插件的type
Model = BookModel.createModel(book);
// BookCollectionShadow 暫時(shí)不懂
Collection.saveBook(book);
ZLTextHyphenator.Instance().load(book.getLanguage());
// 設(shè)置顯示時(shí)的一些屬性
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
}
}
目前能讀懂的都在注釋上,貌似在執(zhí)行 setView(BookTextView)時(shí),就會進(jìn)行渲染的操作了
把幫助文檔當(dāng)成圖書的話, 第一次出現(xiàn)對書的解析應(yīng)該就是在BookUtil的getHelpFile的方法中
BookUtil::getHelpFile()
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");
}
通過固定的路徑,調(diào)用了ZLResourceFile 的 createResourceFile
ZLResourceFile :: createResourceFile(String path)
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;
}
這里有個(gè)簡單的緩存,然后調(diào)用了ZLibrary的createResourceFile方法。ZLibrary是個(gè)抽象類,ZLAndroidLibrary 實(shí)現(xiàn)了它, 并在application中進(jìn)行了初始化操作。
ZLAndroidLibrary::createResourceFile(String path)
@Override
public ZLResourceFile createResourceFile(String path) {
return new AndroidAssetsFile(path);
}
這里,通過文件的路徑,創(chuàng)建了一個(gè)AndroidAssetsFile。AndroidAssetsFile繼承了ZLResourceFile,是ZLFile的子類。

ZLFile 是fbreader對所有文件的同意描述。上圖是繼承樹。
以下是從網(wǎng)絡(luò)上摘取的資料
ResourceFile類專門用來處理資源文件,這一章中要解析的assets文件夾下的資源文件都可以ZLResourceFile類來處理
ZLResourceFile類專門用來處理資源文件,這一章中要解析的assets文件夾下的資源文件都可以ZLResourceFile類來處理。
ZLPhysicalFile類專門用來處理普通文件,eoub文件就可以用一個(gè)ZLPhysicalFile類來代表。
ZLZipEntryFile類用來處理epub文件內(nèi)部的xml文件,這個(gè)類會在第五章“epub文件處理 -- 解壓epub文件”中出現(xiàn)。
這三個(gè)文件類都實(shí)現(xiàn)了getInputStream抽象方法,不用的文件類會通過這個(gè)方法獲得針對當(dāng)前文件類的字節(jié)流類。
AndroidAssetsFile類(ZLResourceFile類的子類)的getInputStream方法會返回AssetInputStream類,這個(gè)類可以將資源文件轉(zhuǎn)換成byte數(shù)組。
ZLPhysicalFile類的getInputStream方法會返回FileInputStream類,這個(gè)類可以將普通的文件轉(zhuǎn)換成byte數(shù)組。
ZLZipEntryFile類的getInputStream方法會返回FileInputStream類,這個(gè)類可以將epub內(nèi)部壓縮過的xml文件轉(zhuǎn)換成可以正常解析的byte數(shù)組
下面看一下AndroidAssetsFile的getInputStream方法??梢圆聹y,讀取幫助文檔的時(shí)候調(diào)用getInputStream會返回這個(gè)文件的InputStream
AndroidAssetsFile:: getInputStream()
@Override
public InputStream getInputStream() throws IOException {
return myApplication.getAssets().open(getPath());
}
得到ZLResourceFile 對象之后,我們回到FBReaderApp::openBook 這個(gè)方法中。
可以看到通過 book = Collection.getBookByFile(BookUtil.getHelpFile());
將ZLResourceFile對象轉(zhuǎn)成了Book 對象了。
Collection是一個(gè)接口IBookCollection, 這里是BookCollectionShadow實(shí)現(xiàn)了這個(gè)接口,在FBReaderApp的onCreate方法,我們可以看到這句

BookCollectionShadow又是什么呢?我們還要往下分析
上面的代碼中創(chuàng)建了一個(gè)FBReaderApp對象, 至于這個(gè)對象是干什么的,現(xiàn)在還不知道。
接下來,getCollection 并 調(diào)用了 bindToService方法
FBReader::getCollection()
private BookCollectionShadow getCollection() {
return (BookCollectionShadow) myFBReaderApp.Collection;
}
調(diào)用的正是這個(gè)主activity的一個(gè)方法。返回的對象是FBReaderApp 的 Collection變量,這個(gè)正式剛才創(chuàng)建的BookCollectionShadow 實(shí)現(xiàn)了IBookCollection接口。
接著調(diào)用了BookCollectionShadow的bindToService方法
BookCollectionShadow::bindToService(Context context, Runnable onBindAction)
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;
}
}
這里執(zhí)行了一句很熟悉的context.bindService方法,這里用到了aidl。這方面的問題就不記錄了,跟主向無關(guān)。
bindService方法有三個(gè)參數(shù),第二個(gè)參數(shù)傳了BookCollectionShadow本身, 我們知道,bindService的第二個(gè)參數(shù)傳的是ServiceConnection接口,在這里面, 我們可以調(diào)用aidl文件生命的方法,進(jìn)行跨進(jìn)程的通訊。也不知道fbreader為什么弄這么多進(jìn)程是想干什么。

果然BookCollectionShadow實(shí)現(xiàn)了ServiceConnection,并且myInterface真是那個(gè)aidl的全局變量, 我們可以通過它,完成與服務(wù)端的溝通。
四、怎樣獲得book對象
經(jīng)過一下午的瞎看,大致的熟悉了一下fbreader的源碼。
帶著問題學(xué)習(xí)總是最快的,那么,fbreader到底是怎樣解析epub文件的呢。我們得找一個(gè)入口。在項(xiàng)目中,長按菜單鍵,會彈出一個(gè)功能列表,會看到一個(gè)本地書柜,一頓操作之后,我們可以找到一個(gè)我事先導(dǎo)入的一個(gè)電子書

最后我們會來到這個(gè)界面

在茫茫碼海中怎么找到這個(gè)activity,用這樣一條命令
adb shell dumpsys activity top | grep ACTIVITY --color

在這個(gè)activitiy中, mybook是通過intent傳入的,所以找上個(gè)頁面LibraryActivity

ListActivity是什么? 沒用過,TreeActivity自己寫的, 太復(fù)雜。想著在LibraryActivity中能找到一些方法
LibraryActivity::onListItemClick(ListView listView, View view, int position, long rowId)
@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);
}
}
在這個(gè)方法中, 是通過tree.getBook();得到的一個(gè)book,點(diǎn)進(jìn)去看了一下
public Book getBook() {
return null;
}
這尼瑪返回空!??!
tree是LibraryTree的一個(gè)實(shí)例,在TreeAdapter的getItem(int position) 方法中得到的
public FBTree getItem(int position) {
return myItems.get(position);
}
存放在了叫private final List<FBTree> myItems;這樣的一個(gè)list之中。
一路尾隨,不是跟蹤myItems, 看一下TreeAdapter的replaceAll方法

根據(jù)名字我們能判斷。得到現(xiàn)在樹的子樹,然后添加到myItem中去的。那么我們知道現(xiàn)在的樹, 或者現(xiàn)在樹的子樹是什么,應(yīng)該就可以得到答案了。
現(xiàn)在的樹集成關(guān)系是這樣的

又在onListItemClick方法中強(qiáng)裝成LibraryTree, 我們只需關(guān)注LibraryTree的之類就行了
這個(gè)樹通過getTreeByKey得到
LibraryActivity::getTreeByKey(FBTree.Key key)
@Override
protected LibraryTree getTreeByKey(FBTree.Key key) {
return key != null ? myRootTree.getLibraryTree(key) : myRootTree;
}
private synchronized void deleteRootTree() {
最后我們要找的這棵樹實(shí)際跟myRootTree有關(guān)。
myRootTree在onCreate的時(shí)候創(chuàng)建
LibraryActivity::onCreate(Bundle icicle)
@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());
}
});
}
在倒數(shù)第二行add的接口的回掉在這里

在
onBookEvent中一定是這個(gè)樹的來源,看來我們要去另一個(gè)進(jìn)程里看看了
這個(gè)服務(wù)和之前的一樣LibraryService。我們跟進(jìn)LibraryService
@Override
public IBinder onBind(Intent intent) {
return myLibrary;
}
返回的真是aidl的一個(gè)接口

以上是接口創(chuàng)建的一部分代碼

在reset中搞了一個(gè)Config.Instance().runOnConnect不知道是干嘛用的,反正是調(diào)用了以下的方法

我注意到底下的兩個(gè)廣播,很明顯是做進(jìn)程間通訊。然后, 我在BookCollectionShadow中找到了這個(gè)廣播的接收者
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
}
}
};
接下來看log

通過這個(gè)<entry xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata"> 搞成一本書???
這個(gè)先別管,我的疑惑是tree.getBook() 怎么返回空?
經(jīng)過上面的彎路, 我們知道了最后回掉的是LibraryActivity:: onBookEvent(BookEvent event, Book book)這個(gè)方法
@Override
public void onBookEvent(BookEvent event, Book book) {
if (getCurrentTree().onBookEvent(event, book)) {
getListAdapter().replaceAll(getCurrentTree().subtrees(), true);
}
}
再跟一下里面的方法
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這個(gè)東西,原來我們正經(jīng)使用的是booktree的getBook,所以get得到的肯定是一本書
這里留一個(gè)問題, 這個(gè)booktree是怎么來的? 先不著急分析他
我們發(fā)現(xiàn)book 是從booktree 得到的, 而booktree中的book 是構(gòu)造是傳進(jìn)來的。
而這些又是在FilteredTree的抽象發(fā)方法createSubtree, 得到的

接著往上看FilteredTree這個(gè)類

終于找到主進(jìn)程里的book了, 原來是調(diào)用進(jìn)程里的 Collection.books(query);方法, 來獲得一個(gè)book的列表。
五、怎么跳轉(zhuǎn)到Fbreader這個(gè)activity
本來想看看怎么解析epub的,但是感覺目前還消化不了。
我們要把這個(gè)完整的項(xiàng)目當(dāng)成一個(gè)sdk來使用,雖然說整個(gè)加到工程中fbreader的5M左右了,但是沒有辦法,時(shí)間緊任務(wù)重。我倒是很贊同自己去寫個(gè)閱讀器,但是條件不允許。
我們要使用這個(gè)項(xiàng)目, 就得找到一個(gè)書, 然后跳轉(zhuǎn)到這個(gè)activity讓他顯示。
經(jīng)過以上的分析, 可以看到,在查找書的時(shí)候 已經(jīng)就裝成book對象了。反正要是我寫,我肯定在查找文件的時(shí)候返回的url, 然后根據(jù)這個(gè)去解析成book的對象。
記得最早之前分析過,在FBReader的onResume是啟動的關(guān)鍵
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;
}
}
這里,但書為空的時(shí)時(shí)候,在intent.getData去 拿到url, 傳到ZLFile里去。也就是說,我在start這個(gè)activity 傳一個(gè)url 進(jìn)去, 于是我這樣寫了一段
/**
* 打開一本電子書
*
* @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);
}
這樣就可以了。 我翻了幾頁之后, 默認(rèn)就會保存閱讀的位置。那么我想重新閱讀這個(gè)文章應(yīng)該怎么辦。繼續(xù)擼代碼
在FBReaderApp::openBookInternal(Book book, Bookmark bookmark, boolean force)方法中,第一次打開書是會執(zhí)行gotoStoredPosition();這樣一個(gè)方法,從字面的意思是去到存儲的位置
具體的方法是這樣的
FBReaderApp::gotoStoredPosition()
private void gotoStoredPosition() {
myStoredPositionBook = Model != null ? Model.Book : null;
if (myStoredPositionBook == null) {
return;
}
myStoredPosition = getStoredPosition(myStoredPositionBook);
BookTextView.gotoPosition(myStoredPosition);
savePosition();
}
是調(diào)用了BookTextView的gotoPosition這個(gè)方法。那我們看看能否在FBReader這個(gè)類上搞點(diǎn)事情。在openBook方法中
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 時(shí) 正常myBook為空 intent.getData 為空
/*
* 在主線程運(yùn)行
*
* 正常打開時(shí)myBook, bookmark, action 三個(gè)參數(shù)都是空
* */
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();
}
});
}
在最后一句,這個(gè)activity中可以拿到myFBReaderApp,猜想是用來控制整個(gè)閱讀器的類, 調(diào)用gotohome, 竟然可以了。通過這個(gè)就可以控制是否繼續(xù)閱讀還是從頭開始
六、跳轉(zhuǎn)到固定章節(jié)
一本書有很多章節(jié),跳轉(zhuǎn)到固定章節(jié)的時(shí)候不可能進(jìn)行一步步的翻頁操作。碰巧fbreader提供這樣的功能,而且還有快速翻看
找打開一本書之后Model 字段就會賦值, ‘彈幕’一下這個(gè)字段, 看到里面確實(shí)存在了章節(jié)的信息

我知道了章節(jié),然后怎么去跳轉(zhuǎn),我們看下面這一段代碼:
TOCActivity::openBookText(TOCTree tree)
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, 然后去調(diào)用BookTextView的gotoPosition
在FBReaderApp中這樣寫試試

這樣是可以達(dá)到預(yù)期效果的,但是有一定要值得注意:
在第一次讀取書時(shí),獲取書的操作是個(gè)異步的, 也就是說,這個(gè)時(shí)候Model可能為空,所以在以后開發(fā)中,最好是用接口,將獲取書的情況反到activity中, 這樣,當(dāng)書加載完成時(shí)再去做相應(yīng)的跳轉(zhuǎn)操作。
七、字體加大與縮小
源碼中,改變字體大小的就在菜單的按鍵中

點(diǎn)擊按鍵監(jiān)聽再這里,這么搞也是特殊,從未見過啊,不知道干嘛弄的這么復(fù)雜。像這樣可以實(shí)現(xiàn)這個(gè)功能了。
這樣只是增加與減少,萬一需求上是給定幾個(gè)固定的字號,然后調(diào)節(jié)怎么辦?所以開始得看看源碼

跟進(jìn)去發(fā)現(xiàn),最終的action是存在于這個(gè)map, 實(shí)在主activity創(chuàng)建時(shí)put的,所以所有的操作都會交給ZLAction的子類去處理

也就是上面選中的這個(gè)類
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();
}
}
執(zhí)行run方法后會設(shè)置字號,這里的option.setValue(option.getValue() + myDelta);就是對字號的設(shè)置。要是愿意的話可以復(fù)寫FBAction或者直接使用run方法中的參數(shù)進(jìn)行傳值,當(dāng)然,后一種要好一點(diǎn)。
八、音量鍵功能
一個(gè)功能完整的閱讀器,音量鍵也都會派上用場。fbreader音量鍵也不例外
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;
}
}
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 是當(dāng)前案件的鍵碼,會對所有按鍵處理,如果你接鍵盤的話。
longPress 字面意思是是否長按,但是我試過永遠(yuǎn)的短按,永遠(yuǎn)的false
以后處理案件就可在這里處理,或者深入到action里面進(jìn)行處理。
九、更換背景,字體顏色
更換背景以及字體顏色,也是實(shí)現(xiàn)夜間模式的一個(gè)套路。首先我們定位到PreferenceActivity,冷不丁一看,我去,這不是系統(tǒng)里的settings嘛。這么長的init至于么?

這么長的代碼就不全粘貼了,大概在400多行,有這么一段。是添加背景和墻紙的
我們跟到這個(gè)類中BackgroundPreference
在onBindView下面是跳轉(zhuǎn)到顏色選擇器的代碼,在PreferenceActivity中的onActivityResult方法中返回
@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);
}
中間部分, 調(diào)用了yBackgroundPreference.update(data);
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();
}
繼續(xù)往里跟,方向設(shè)置顏色或者是壁紙圖片, 只是設(shè)置了WallpaperOption的value。我們能在ColorProfile類中找到這些參數(shù),那么我們怎么在主activity獲取并且改變它呢
還是最重要的FBReaderApp里面有ViewOptions 的實(shí)例 ViewOptions,通過它我們就能拿到ColorProfile, 圖下設(shè)置背景為紅色
myFBReaderApp.ViewOptions.getColorProfile().BackgroundOption.setValue(new ZLColor(255, 0, 0));
但是直接這么寫,沒有變化,仔細(xì)想想。我只設(shè)置了顏色,但是沒有通知重繪,自然就沒有變化。
那么,顏色選擇怎么通知到這個(gè)activity的呢,只能是Intent傳的,我們看下FBReader 的onActivityResult(int requestCode, int resultCode, Intent data)
@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, 確實(shí)有我需要的東西,這個(gè)方法調(diào)用了onPreferencesUpdate
private void onPreferencesUpdate(Book book) {
AndroidFontUtil.clearFontCache();
myFBReaderApp.onBookUpdated(book);
}
\ 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();
}
}
起到?jīng)Q定性作用的就行最后一句 getViewWidget().repaint();
那么,更改背景顏色就是
(上面的代碼都是等書加載完畢之后,顯示在view中在去設(shè)置的, 不然書都沒加載完,自然也就設(shè)置不了)

BackgroundOption 是背景色
RegularTextOption 是文字的顏色
十、動畫類型
下面開始研究應(yīng)用的翻頁動畫。
我們修改顏色實(shí)際上是修改了ZLAndroidWidget。我們可以跟進(jìn)這個(gè)類看一下代碼
ZLAndroidWidget::getAnimationProvider()
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;
}
代碼跟你的不一樣,正常,這個(gè)我改過了。
在繪制動畫的時(shí)候,也就是onDrawInScrolling(Canvas canvas)這個(gè)方法被調(diào)用的時(shí)候,都會獲取一下當(dāng)前的動畫。
那么在設(shè)置動畫的時(shí)候調(diào)用的是PreferenceActivity這個(gè)activity,再init一堆東西里看以看到,這樣一句話

原來是改變的是pageTurningOptions這個(gè)東西,我們再看主activity中能不能找到這個(gè)對象?;氐紽BReader當(dāng)中。我們可以這樣設(shè)置動畫
myFBReaderApp.PageTurningOptions.Animation.setValue(ZLView.Animation.left2right);
因?yàn)槭菆?zhí)行每一次翻頁的動作都會get一下當(dāng)前的動畫,所以也就不需要重繪當(dāng)前頁面,寫這樣一句就好。
十一、點(diǎn)擊區(qū)域
市場上的閱讀類應(yīng)用?;径际屈c(diǎn)擊左面上一頁,右面下一頁。中間會彈出一個(gè)設(shè)置菜單。
我發(fā)現(xiàn)我的這個(gè)version的代碼,點(diǎn)擊中間是沒有任何反應(yīng)的,所以。繼續(xù)擼碼。
關(guān)于點(diǎn)擊時(shí)間, 首先去看ZLAndroidWidget的onTouchEvent方法
ZLAndroidWidget::onTouchEvent(MotionEvent event)
@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;
}
當(dāng)按鍵抬起的時(shí)候, 將手指的位置傳給了 view.onFingerSingleTap(x, y);
view就是FBView這個(gè)類。
@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;
}
這段的最后一句就是或坐標(biāo)的區(qū)域
一路跟下去, 發(fā)現(xiàn)TapZoneMap 這樣一個(gè)類,看它的構(gòu)造
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);
}
是讀了一個(gè)文件,我們在資源文件中找下

果然在這里躺著一堆的文件,應(yīng)該是按照屏幕的方向 選擇默認(rèn)的配置文件,這里默認(rèn)的就是right_to_left.xml,我們打開看看。
<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應(yīng)該是把屏幕分成3 x 3的區(qū)域,大概就是上面圖片的意思。用這個(gè)坐標(biāo)代表,我要點(diǎn)擊屏幕中間的,自然我就加了一個(gè)1,1的坐標(biāo)。
得到區(qū)域后,執(zhí)行了FBView:: myReader.runAction(actionId, x, y); 這個(gè)方法。
跟音量鍵的一樣,他會把事件分發(fā)的ShowMenuAction這個(gè)類里面。
class ShowMenuAction extends FBAndroidAction {
ShowMenuAction(FBReader baseActivity, FBReaderApp fbreader) {
super(baseActivity, fbreader);
}
@Override
protected void run(Object ... params) {
BaseActivity.openOptionsMenu();
//BaseActivity.menu();
}
}
源碼里調(diào)用的是openOptionsMenu,這個(gè)baseactivity就是我們的FBReader這個(gè)類。我改成自己的方法, 就可以定制自己的菜單欄了,然后拋棄源碼提供的菜單欄。
ps:代碼看到這里差不多可以進(jìn)行定制了, 但是,代碼里確實(shí)是有些無用的東西, 要是能把這些東西去掉的話,做一下精簡。應(yīng)該會更好。