开源电子书项目FBReader初探(二)

2,867 阅读9分钟

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是怎么去响应用户单击事件的了。而且也发现了诸如切换日夜间模式、设置阅读页面朝向、打开书籍目录、书籍书签等等一系列操作的定义,也就可以开始进行一些简单的设置处理了。

当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。谢谢!下一章,我们就去看一下,我们能通过什么办法打开一本书,以及在一本书打开之前,都经历了些什么。