内容简介:本来是想要探索FBReader是如何打开一本书的,但是发现涉及到的方方面面特别的多,索性我们就来细细拆解,根据使用FBReader的步骤,循序渐进的去品位FBReader这个庞大的工程到底是怎么运作的。想要对FBReader进行进一步的分析,首先要学会如何去使用这款软件,知道它都有哪些功能提供给用户。经过第一篇简单的导入和相关设置,相信大伙已经能够顺利运行app,那我们就愉快的run起来吧。App运行起来之后,是这个样子的,朴实的外表泥土的芬芳。
本来是想要探索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是怎么去响应用户单击事件的了。而且也发现了诸如切换日夜间模式、设置阅读页面朝向、打开书籍目录、书籍书签等等一系列操作的定义,也就可以开始进行一些简单的设置处理了。
当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。谢谢!下一章,我们就去看一下,我们能通过什么办法打开一本书,以及在一本书打开之前,都经历了些什么。
以上所述就是小编给大家介绍的《开源电子书项目FBReader初探(二)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 开源电子书项目FBReader初探(一)
- 开源电子书项目FBReader初探(三)
- 开源电子书项目FBReader初探(四)
- 开源电子书项目FBReader初探(五)
- 开源电子书项目FBReader初探(六)
- 初探 Google 开源的 Python 命令行库
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。