1. 程式人生 > >開源電子書專案FBReader初探(二)

開源電子書專案FBReader初探(二)

FBReader第一次接觸,開啟選單

一、FBReader是如何處理使用者的“第一個有效”點選事件,並將其轉換成對應actionId呢?

本來是想要探索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是怎麼去響應使用者單擊事件的了。而且也發現了諸如切換日夜間模式、設定閱讀頁面朝向、開啟書籍目錄、書籍書籤等等一系列操作的定義,也就可以開始進行一些簡單的設定處理了。

當然,由於本人接觸此專案時間有限,而且書寫技術文章的經驗實在欠缺,過程中難免會有存在錯誤或描述不清或語言累贅等等一些問題,還望大家能夠諒解,同時也希望大家繼續給予指正。最後,感謝大家對我的支援,讓我有了強大的動力堅持下去。謝謝!下一章,我們就去看一下,我們能通過什麼辦法開啟一本書,以及在一本書開啟之前,都經歷了些什麼。