開源電子書專案FBReader初探(二)
本來是想要探索FBReader是如何開啟一本書的,但是發現涉及到的方方面面特別的多,索性我們就來細細拆解,根據使用FBReader的步驟,循序漸進的去品位FBReader這個龐大的工程到底是怎麼運作的。
想要對FBReader進行進一步的分析,首先要學會如何去使用這款軟體,知道它都有哪些功能提供給使用者。經過第一篇簡單的匯入和相關設定,相信大夥已經能夠順利執行app,那我們就愉快的run起來吧。
App執行起來之後,是這個樣子的,樸實的外表泥土的芬芳。

當然了,這個app在操作的時候,是要點選一塊固定的區域,才能彈出來一個操作選單,進而去執行其他的操作,為了標識出這塊區域,就給它按照view的座標系方向,來做一下標記:

在清單檔案,可以發現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是怎麼去響應使用者單擊事件的了。而且也發現了諸如切換日夜間模式、設定閱讀頁面朝向、開啟書籍目錄、書籍書籤等等一系列操作的定義,也就可以開始進行一些簡單的設定處理了。
當然,由於本人接觸此專案時間有限,而且書寫技術文章的經驗實在欠缺,過程中難免會有存在錯誤或描述不清或語言累贅等等一些問題,還望大家能夠諒解,同時也希望大家繼續給予指正。最後,感謝大家對我的支援,讓我有了強大的動力堅持下去。謝謝!下一章,我們就去看一下,我們能通過什麼辦法開啟一本書,以及在一本書開啟之前,都經歷了些什麼。