内容简介:本来是想要探索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 命令行库
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Designing for Emotion
Aarron Walter / Happy Cog / 2011-10-18 / USD 18.00
Make your users fall in love with your site via the precepts packed into this brief, charming book by MailChimp user experience design lead Aarron Walter. From classic psychology to case studies, high......一起来看看 《Designing for Emotion》 这本书的介绍吧!