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

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

转载于:https://juejin.im/post/5bf67a3a51882518eb1f5f32

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FBReader 是一种快速且高度可定制的电子书阅读器,适用于运行 Android OS 的设备(支持 1.5 或更高版本,建议 2.0 或更高版本)。 FBReader Premium App 的免费版本是标准版本,具有付费版本的所有功能。唯一的区别是 4.99 美元的溢价价格。 如果您已经在免费使用该应用程序,那么价格似乎有点高。 但是,它仍然提供与常规 FBReader 版本的用户所喜欢的相同的快速,简单的信息访问方式。 安卓电子书阅读器 FBReader 中文多语免费版安卓电子书阅读器 FBReader 中文多语免费版 FBReader 可用功能 大声朗读(通过 Android 文本语音转换) Google 翻译集成 内置对 PDF 和漫画格式的支持 缩略图库视图 主要电子书格式:ePub(包括 ePub3 的主要功能),PDF,Kindle azw3(mobipocket),fb2(.zip)。 其他受支持的格式:漫画书(CBR / CBZ),RTF,doc(MS Word),HTML,纯文本。 为了帮助您阅读外语,请使用 FBReader 的词典集成来查找单词或短语。您可以从多种外部词典中进行选择。 在 FBReader 中,您可以使用集成的 Google 或 Yandex 转换器翻译句子而无需离开应用程序。 FBReader 支持您的图书馆和/或阅读位置与 FBReader 图书网络(https://books.fbreader.org/)的同步,这是一种基于 Google Drive™ 的云服务。默认情况下,同步是禁用的;要启用和配置它,请使用“首选项”对话框。 FBReader 快速且高度可定制-它可以使用外部 TrueType / OpenType 字体和自定义背景,可以在读取时调节屏幕亮度(沿屏幕左边缘向上/向下滑动手指),并且可以选择不同的昼/夜配色方案。 该阅读器还包括一个浏览器/下载器,用于访问不同的网络电子书目录和商店。包括几个流行的英语,法语,俄语,中文和波兰语库。还支持自定义 OPDS 目录。 或者,您可以手动下载书籍,然后将其保存在设备上的 /sdcard/Books 中。 此外,该阅读器已针对 34 种语言进行了本地化,并包括针对 24 种语言的连字符模式。
主要电子书格式:ePub(包括ePub3的主要功能),PDF,Kindle azw3(mobipocket),fb2(.zip)。其他支持的格式:漫画书(CBR / CBZ),RTF,doc(MS Word),html,plain文本。 升级到这个流行的电子书阅读器的高级版。 此高级版本提供的功能: * Google / Yandex翻译集成 *内置支持PDF和漫画书格式 *缩略图库视图 为了帮助阅读外语,请使用FBReader的字典集成来查找单词或短语。您可以从众多外部词典中进行选择。 在FBReader Premium中,您可以使用集成的Google或Yandex翻译器在不离开应用程序的情况下翻译句子。(警告:此功能的每日使用可能会受到限制。) FBReader支持使用基于Google Drive™的云服务FBReader图书网络(https://books.fbreader.org/)同步您的图书馆和/或阅读位置。默认情况下禁用同步; 要启用和配置它,请使用首选项对话框。 FBReader快速且高度可定制 - 它可以使用外部TrueType / OpenType字体和自定义背景,可以在阅读时调整屏幕亮度(沿着左侧屏幕边缘向上/向下滑动手指),并且可以选择不同的日/夜配色方案。 该阅读器还包括一个浏览器/下载器,用于访问不同的网络电子书目录和商店。包括几个流行的英语,法语,俄语,中文和波兰语库。也支持自定义OPDS目录。 或者,您可以手动下载书籍并将其保存在/ sdcard / Books中的设备上。 此外,该阅读器本地化为34种语言,包括24种语言的连字模式。该应用程序是开源(GPL)。 什么是新的 内置文字转语音模块 文件选择器 新升商店目录 重大内部变化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值