開源電子書專案FBReader初探(二)
FBReader第一次接觸,開啟選單
一、FBReader是如何處理使用者的“第一個有效”點選事件,並將其轉換成對應actionId呢?
本來是想要探索FBReader是如何開啟一本書的,但是發現涉及到的方方面面特別的多,索性我們就來細細拆解,根據使用FBReader的步驟,循序漸進的去品位FBReader這個龐大的工程到底是怎麼運作的。
想要對FBReader進行進一步的分析,首先要學會如何去使用這款軟體,知道它都有哪些功能提供給使用者。經過第一篇簡單的匯入和相關設定,相信大夥已經能夠順利執行app,那我們就愉快的run起來吧。
App執行起來之後,是這個樣子的,樸實的外表泥土的芬芳。
<?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
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是怎麼去響應使用者單擊事件的了。而且也發現了諸如切換日夜間模式、設定閱讀頁面朝向、開啟書籍目錄、書籍書籤等等一系列操作的定義,也就可以開始進行一些簡單的設定處理了。
當然,由於本人接觸此專案時間有限,而且書寫技術文章的經驗實在欠缺,過程中難免會有存在錯誤或描述不清或語言累贅等等一些問題,還望大家能夠諒解,同時也希望大家繼續給予指正。最後,感謝大家對我的支援,讓我有了強大的動力堅持下去。謝謝!下一章,我們就去看一下,我們能通過什麼辦法開啟一本書,以及在一本書開啟之前,都經歷了些什麼。