開源電子書項目FBReader初探(二)

FBReader第一次接觸,打開菜單

1、FBReader是如何處理用戶的「第一個有效」點擊事件,並將其轉換成對應actionId呢?

原本是想要探索FBReader是如何打開一本書的,可是發現涉及到的方方面面特別的多,索性咱們就來細細拆解,根據使用FBReader的步驟,按部就班的去品位FBReader這個龐大的工程究竟是怎麼運做的。android

想要對FBReader進行進一步的分析,首先要學會如何去使用這款軟件,知道它都有哪些功能提供給用戶。通過第一篇簡單的導入和相關設置,相信大夥已經可以順利運行app,那咱們就愉快的run起來吧。app

App運行起來以後,是這個樣子的,樸實的外表泥土的芬芳。 ide

固然了,這個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)這個區域的時候纔可以彈出來操做菜單:post

剛纔咱們看過佈局文件,知道了FBReader這個Activity的佈局中只有一個核心控件ZLAndroidWidget,並且從這個特殊行爲(只有點 1,2 區域才彈出菜單)來看,應該是在觸摸事件的處理過程當中,判斷了用戶點擊的區域才作出相應的行爲,究竟是不是這樣呢?咱們直接進入ZLAndroidWidget,去一探究竟。動畫

ZLAndroidWidget對點擊區域的特殊處理

咱們直接來看它的onTouchEvent方法,鑑於關注的是點擊事件,直接瞅準action up :ui

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):this

public void onFingerSingleTap(int x, int y) {
    // 上面的代碼省略...   
    onFingerSingleTapLastResort(x, y);
}
複製代碼

進入onFingerSingleTapLastResort(x,y),這裏須要注意一個點,判斷了是否支持雙擊操做isDoubleTapSupported(),而且根據結果判斷傳遞到後續的tap類型,這有什麼用呢?暫且先無論,先看:spa

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"。

2、有了「有效操做」對應的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是怎麼去響應用戶單擊事件的了。並且也發現了諸如切換日夜間模式、設置閱讀頁面朝向、打開書籍目錄、書籍書籤等等一系列操做的定義,也就能夠開始進行一些簡單的設置處理了。

固然,因爲本人接觸此項目時間有限,並且書寫技術文章的經驗實在欠缺,過程當中不免會有存在錯誤或描述不清或語言累贅等等一些問題,還望你們可以諒解,同時也但願你們繼續給予指正。最後,感謝你們對個人支持,讓我有了強大的動力堅持下去。謝謝!下一章,咱們就去看一下,咱們能經過什麼辦法打開一本書,以及在一本書打開以前,都經歷了些什麼。

相關文章
相關標籤/搜索