FBREADER 源码阅读笔记 前言 这篇文章是我在读源码时候的笔记。是我的一个习惯吧!在阅读源码的时候会记录一下思路,省得自己会忘记,相当于“保护现场”了吧。由于是一边看代码一边记录,一定会有很多的错误,请大家见谅。
一、代码导入 在 https://github.com/geometer/FBReaderJ 这个地址上就是fbreader的java项目。
版本库上面的项目是eclipse编写的,所以第一步,想办法把这个项目变成AS上开发的
(因为种种原因,我并没有clone github上的源码,在我们svn存在着之前的一个fbreader版本,是2.0的)
1、目的 导入fbreader这个项目是想在自己的程序中加入阅读器的功能。但从头开始开发时间长,并且没做过。所以参考了fbreader,在源码的基础上做二次开发。
代码同步下来之后,发现。fbreader并不是一个开源库(SDK),而是一个完整的项目,部分功能使用了jni开发,并且支持插件化,通过aidl,进行组件之间的通讯
因为要导入现有的项目(重构ing),想法是将fbreader整个项目编译成aar,然后导入现有项目。因为时间短,起初可以先把整个fbreader导入,之后对fbreader研究之后,或去掉相应模块或者重新开发
2、导入 导入过程比较繁琐,又没什么技术含量。主要就是将之前ant构建的项目换成gradle构建。
这里设计jni和aidl的目录结构有所变化,按照android studio上面的结构统一创建就好
(这里我犯了一个错误,fbreader的主项目千万别改报名,就按照之前的来,否则要改掉很多文件的import,相当费事儿。千万别改,千万别改,千万别改)
3、运行 先用ndk-build编出so, 然后在导入或者直接用android studio 带c++ 一起编, 都可以。反正 studio 也支持编译c语言了。
这里我直接使用ndk-build 编出so库,然后将so库添加到我的项目中去。省着clean项目的时候还要去重新编译,挺耗时间的。
二、源码目录 源码中的目录结构,其实我是在公司的svn里看到的,不知道谁写的,看时间,写这个文章的时候我刚上大学。
我就直接撸过来了。
用红笔画掉的是fbreader的一些三方以来,fbreader是主要的源码目录,
app 是我用来模仿公司的主项目的,其实就是一句startActivity。
以下是源码的一些目录
jni 的文件目录
整个项目的, 大概看一看
三、无目的的瞎看 网上的资源不是很多,项目也较老了。再加上从未接触过阅读相关的项目。扎铁了老心。没有头绪就从头看代码吧。
再 AndroidManifest 能知道应用的主activity是 org.geometerplus.android.fbreader.FBReader
(这里强插一句, 想运行fbreader这个项目,application是要继承自FBReaderApplication;还要修改FBReaderIntents类的第一行的包名,保持与项目的包名一致)
在FBReader先无目的看一下 FBReader::onCreate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 @Override protected void onCreate (Bundle icicle) { super .onCreate(icicle); Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(this )); bindService( new Intent(this , DataService.class), DataConnection, DataService.BIND_AUTO_CREATE ); final Config config = Config.Instance(); config.runOnConnect(new Runnable() { public void run () { config.requestAllValuesForGroup("Options" ); config.requestAllValuesForGroup("Style" ); config.requestAllValuesForGroup("LookNFeel" ); config.requestAllValuesForGroup("Fonts" ); config.requestAllValuesForGroup("Colors" ); config.requestAllValuesForGroup("Files" ); } }); final ZLAndroidLibrary zlibrary = getZLibrary(); myShowStatusBarFlag = zlibrary.ShowStatusBarOption.getValue(); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.main); myRootView = (RelativeLayout) findViewById(R.id.root_view); myMainView = (ZLAndroidWidget) findViewById(R.id.main_view); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); zlibrary.setActivity(this ); myFBReaderApp = (FBReaderApp) FBReaderApp.Instance(); if (myFBReaderApp == null ) { myFBReaderApp = new FBReaderApp(new BookCollectionShadow()); } getCollection().bindToService(this , null ); myBook = null ; myFBReaderApp.setWindow(this ); myFBReaderApp.initWindow(); myFBReaderApp.setExternalFileOpener(new ExternalFileOpener(this )); getWindow().setFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN, myShowStatusBarFlag ? 0 : WindowManager.LayoutParams.FLAG_FULLSCREEN ); if (myFBReaderApp.getPopupById(TextSearchPopup.ID) == null ) { new TextSearchPopup(myFBReaderApp); } if (myFBReaderApp.getPopupById(NavigationPopup.ID) == null ) { new NavigationPopup(myFBReaderApp); } if (myFBReaderApp.getPopupById(SelectionPopup.ID) == null ) { new SelectionPopup(myFBReaderApp); } 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)); 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)); myFBReaderApp.addAction(ActionCode.PROCESS_HYPERLINK, new ProcessHyperlinkAction(this , myFBReaderApp)); myFBReaderApp.addAction(ActionCode.OPEN_VIDEO, new OpenVideoAction(this , myFBReaderApp)); myFBReaderApp.addAction(ActionCode.SHOW_CANCEL_MENU, new ShowCancelMenuAction(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 (ZLibrary.Instance().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)); final Intent intent = getIntent(); final String action = intent.getAction(); myOpenBookIntent = intent; if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0 ) { if (FBReaderIntents.Action.CLOSE.equals(action)) { myCancelIntent = intent; myOpenBookIntent = null ; } else if (FBReaderIntents.Action.PLUGIN_CRASH.equals(action)) { myFBReaderApp.ExternalBook = null ; myOpenBookIntent = null ; getCollection().bindToService(this , new Runnable() { public void run () { myFBReaderApp.openBook(null , null , null ); } }); } } }
onCreate的代码好长一堆,还什么都看不懂。最后一段貌似是openBook 的操作, 但是intent和action 都是空的,根本不执行FBReader也不存在父类,只能是在onResume() 中了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 @Override protected void onResume () { super .onResume(); SyncOperations.enableSync(this , true ); myStartTimer = true ; Config.Instance().runOnConnect(new Runnable() { public void run () { final int brightnessLevel = getZLibrary().ScreenBrightnessLevelOption.getValue(); if (brightnessLevel != 0 ) { setScreenBrightness(brightnessLevel); } else { setScreenBrightnessAuto(); } if (getZLibrary().DisableButtonLightsOption.getValue()) { setButtonLight(false ); } getCollection().bindToService(FBReader.this , new Runnable() { public void run () { final BookModel model = myFBReaderApp.Model; if (model == null || model.Book == null ) { return ; } onPreferencesUpdate(myFBReaderApp.Collection.getBookById(model.Book.getId())); } }); } }); registerReceiver(myBatteryInfoReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); IsPaused = false ; myResumeTimestamp = System.currentTimeMillis(); if (OnResumeAction != null ) { final Runnable action = OnResumeAction; OnResumeAction = null ; action.run(); } registerReceiver(mySyncUpdateReceiver, new IntentFilter(SyncOperations.UPDATED)); SetScreenOrientationAction.setOrientation(this , ZLibrary.Instance().getOrientationOption().getValue()); LogUtils.d("FBReader -> onResume cancelIntent: " + myCancelIntent); LogUtils.d("FBReader -> onResume myOpenBookIntent: " + myOpenBookIntent); if (myCancelIntent != null ) { final Intent intent = myCancelIntent; myCancelIntent = null ; getCollection().bindToService(this , new Runnable() { public void run () { runCancelAction(intent); } }); return ; } else if (myOpenBookIntent != null ) { final Intent intent = myOpenBookIntent; myOpenBookIntent = null ; getCollection().bindToService(this , new Runnable() { public void run () { openBook(intent, null , true ); } }); } else if (myFBReaderApp.getCurrentServerBook() != null ) { getCollection().bindToService(this , new Runnable() { public void run () { myFBReaderApp.useSyncInfo(true ); } }); } else if (myFBReaderApp.Model == null && myFBReaderApp.ExternalBook != null ) { getCollection().bindToService(this , new Runnable() { public void run () { myFBReaderApp.openBook(myFBReaderApp.ExternalBook, null , null ); } }); } else { getCollection().bindToService(this , new Runnable() { public void run () { myFBReaderApp.useSyncInfo(true ); } }); } PopupPanel.restoreVisibilities(myFBReaderApp); ApiServerImplementation.sendEvent(this , ApiListener.EVENT_READ_MODE_OPENED); }
通过一顿的输出log并且debug代码,发现执行了onResume中最后的几个分支语句的第二个分支。
bindToService 这个是什么? 先不管它,往下看
接下来调用了
1 FBReader::openBook(Intent intent, final Runnable action, boolean force)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 private synchronized void openBook (Intent intent, final Runnable action, boolean force) { if (!force && myBook != null ) { return ; } myBook = FBReaderIntents.getBookExtra(intent); final Bookmark bookmark = FBReaderIntents.getBookmarkExtra(intent); LogUtils.d("FBReader -> openBook myBook: " + myBook); if (myBook == null ) { final Uri data = intent.getData(); LogUtils.d("FBReader -> openBook data: " + data); if (data != null ) { myBook = createBookForFile(ZLFile.createFileByPath(data.getPath())); } } if (myBook != null ) { ZLFile file = myBook.File; LogUtils.d("FBReader -> openBook file path: " + file.getPath()); LogUtils.d("FBReader -> openBook file exists: " + file.exists()); if (!file.exists()) { if (file.getPhysicalFile() != null ) { file = file.getPhysicalFile(); } UIUtil.showErrorMessage(this , "fileNotFound" , file.getPath()); myBook = null ; } } Config.Instance().runOnConnect(new Runnable() { public void run () { LogUtils.d("FBReader -> openBook run thread: " + Thread.currentThread()); LogUtils.d("FBReader -> openBook run myBook: " + myBook); LogUtils.d("FBReader -> openBook run bookmark: " + bookmark); LogUtils.d("FBReader -> openBook run action: " + action); myFBReaderApp.openBook(myBook, bookmark, action); AndroidFontUtil.clearFontCache(); } }); }
直接能跟到最后几行的 myFBReaderApp.openBook(myBook, bookmark, action); 这一句
log输出,这三个参数都是空的。执行了FBReaderApp的openBook方法
1 FBReaderApp::(Book book, final Bookmark bookmark, Runnable postAction)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public void openBook (Book book, final Bookmark bookmark, Runnable postAction) { LogUtils.d("FBReaderApp -> openBook: " + Model); if (Model != null ) { if (book == null || bookmark == null && book.File.equals(Model.Book.File)) { return ; } } if (book == null ) { book = getCurrentServerBook(); if (book == null ) { showBookNotFoundMessage(); book = Collection.getRecentBook(0 ); } if (book == null || !book.File.exists()) { book = Collection.getBookByFile(BookUtil.getHelpFile()); } if (book == null ) { return ; } } final Book bookToOpen = book; bookToOpen.addLabel(Book.READ_LABEL); Collection.saveBook(bookToOpen); LogUtils.d("FBReaderApp -> openBook bookToOpen: " + bookToOpen); final SynchronousExecutor executor = createExecutor("loadingBook" ); executor.execute(new Runnable() { public void run () { openBookInternal(bookToOpen, bookmark, false ); } }, postAction); }
三个参数,大体上能猜测出是什么意思,但是,并不是很清晰。 执行到getCurrentServerBook一句时,但我们第一次启动应用是,此时的book对象是空,即使是getCurrentServerBook执行完之后还是空的。之后便去找到这个帮助文档getHelpFile。 然后转化成book对象。 之后,貌似创建了线程。
SynchronousExecutor这个东西是个接口由ZLApplication:: createExecutor(String key) 创建
1 ZLApplication:: createExecutor(String key)
1 2 3 4 5 6 7 protected SynchronousExecutor createExecutor (String key) { if (myWindow != null ) { return myWindow.createExecutor(key); } else { return myDummyExecutor; } }
这里调用了myWindow的createExecutor方法,myWindow(ZLApplicationWindow)是 一个接口,FBReader实现了这个接口。 接着,调用了UIUtil的createExecutor方法
1 UIUtil::createExecutor(final Activity activity, final String key)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public static ZLApplication.SynchronousExecutor createExecutor (final Activity activity, final String key) { return new ZLApplication.SynchronousExecutor() { private final ZLResource myResource = ZLResource.resource("dialog" ).getResource("waitMessage" ); private final String myMessage = myResource.getResource(key).getValue(); private volatile ProgressDialog myProgress; public void execute (final Runnable action, final Runnable uiPostAction) { activity.runOnUiThread(new Runnable() { public void run () { myProgress = ProgressDialog.show(activity, null , myMessage, true , false ); final Thread runner = new Thread() { public void run () { action.run(); activity.runOnUiThread(new Runnable() { public void run () { try { myProgress.dismiss(); myProgress = null ; } catch (Exception e) { e.printStackTrace(); } if (uiPostAction != null ) { uiPostAction.run(); } } }); } }; runner.setPriority(Thread.MAX_PRIORITY); runner.start(); } }); } private void setMessage (final ProgressDialog progress, final String message) { if (progress == null ) { return ; } activity.runOnUiThread(new Runnable() { public void run () { progress.setMessage(message); } }); } public void executeAux (String key, Runnable runnable) { setMessage(myProgress, myResource.getResource(key).getValue()); runnable.run(); setMessage(myProgress, myMessage); } }; }
主要看execute方法,这里先显示一个进度框,然后执行第一个参数action ,然后关闭进度框,这个action 就是在 FBReaderApp::openBook(Book book, final Bookmark bookmark, Runnable postAction) 中的
1 FBReaderApp::openBookInternal(bookToOpen, bookmark, false );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 private synchronized void openBookInternal (Book book, Bookmark bookmark, boolean force) { LogUtils.d("FBReaderApp -> openBookInternal bookmark: " + bookmark); if (!force && Model != null && book.equals(Model.Book)) { if (bookmark != null ) { gotoBookmark(bookmark, false ); } return ; } onViewChanged(); storePosition(); BookTextView.setModel(null ); FootnoteView.setModel(null ); clearTextCaches(); Model = null ; ExternalBook = null ; System.gc(); System.gc(); final FormatPlugin plugin = book.getPluginOrNull(); LogUtils.d("FBReaderApp -> openBookInternal plugin: " + plugin); if (plugin instanceof ExternalFormatPlugin) { ExternalBook = book; final Bookmark bm; if (bookmark != null ) { bm = bookmark; } else { ZLTextPosition pos = getStoredPosition(book); if (pos == null ) { pos = new ZLTextFixedPosition(0 , 0 , 0 ); } bm = new Bookmark(book, "" , pos, pos, "" , false ); } myExternalFileOpener.openFile((ExternalFormatPlugin) plugin, book, bm); return ; } try { Model = BookModel.createModel(book); Collection.saveBook(book); ZLTextHyphenator.Instance().load(book.getLanguage()); BookTextView.setModel(Model.getTextModel()); setBookmarkHighlightings(BookTextView, null ); gotoStoredPosition(); if (bookmark == null ) { setView(BookTextView); } else { gotoBookmark(bookmark, false ); } Collection.addBookToRecentList(book); final StringBuilder title = new StringBuilder(book.getTitle()); if (!book.authors().isEmpty()) { boolean first = true ; for (Author a : book.authors()) { title.append(first ? " (" : ", " ); title.append(a.DisplayName); first = false ; } title.append(")" ); } setTitle(title.toString()); } catch (BookReadingException e) { processException(e); } getViewWidget().reset(); getViewWidget().repaint(); try { for (FileEncryptionInfo info : book.getPlugin().readEncryptionInfos(book)) { if (info != null && !EncryptionMethod.isSupported(info.Method)) { showErrorMessage("unsupportedEncryptionMethod" , book.File.getPath()); break ; } } } catch (BookReadingException e) { } }
目前能读懂的都在注释上,貌似在执行 setView(BookTextView) 时,就会进行渲染的操作了
把帮助文档当成图书的话, 第一次出现对书的解析应该就是在BookUtil的getHelpFile的方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static ZLResourceFile getHelpFile () { final Locale locale = Locale.getDefault(); ZLResourceFile file = ZLResourceFile.createResourceFile( "data/help/MiniHelp." + locale.getLanguage() + "_" + locale.getCountry() + ".fb2" ); if (file.exists()) { return file; } file = ZLResourceFile.createResourceFile( "data/help/MiniHelp." + locale.getLanguage() + ".fb2" ); if (file.exists()) { return file; } return ZLResourceFile.createResourceFile("data/help/MiniHelp.en.fb2" ); }
通过固定的路径,调用了ZLResourceFile 的 createResourceFile
1 ZLResourceFile :: createResourceFile(String path)
1 2 3 4 5 6 7 8 public static ZLResourceFile createResourceFile (String path) { ZLResourceFile file = ourCache.get(path); if (file == null ) { file = ZLibrary.Instance().createResourceFile(path); ourCache.put(path, file); } return file; }
这里有个简单的缓存,然后调用了ZLibrary的createResourceFile方法。ZLibrary是个抽象类,ZLAndroidLibrary 实现了它, 并在application中进行了初始化操作。
1 ZLAndroidLibrary::createResourceFile(String path)
1 2 3 4 @Override public ZLResourceFile createResourceFile (String path) { return new AndroidAssetsFile(path); }
这里,通过文件的路径,创建了一个AndroidAssetsFile。AndroidAssetsFile继承了ZLResourceFile,是ZLFile的子类。
ZLFile 是fbreader对所有文件的同意描述。上图是继承树。
以下是从网络上摘取的资料
ResourceFile类专门用来处理资源文件,这一章中要解析的assets文件夹下的资源文件都可以ZLResourceFile类来处理
ZLResourceFile类专门用来处理资源文件,这一章中要解析的assets文件夹下的资源文件都可以ZLResourceFile类来处理。
ZLPhysicalFile类专门用来处理普通文件,eoub文件就可以用一个ZLPhysicalFile类来代表。
ZLZipEntryFile类用来处理epub文件内部的xml文件,这个类会在第五章“epub文件处理 – 解压epub文件”中出现。
这三个文件类都实现了getInputStream抽象方法,不用的文件类会通过这个方法获得针对当前文件类的字节流类。
AndroidAssetsFile类(ZLResourceFile类的子类)的getInputStream方法会返回AssetInputStream类,这个类可以将资源文件转换成byte数组。
ZLPhysicalFile类的getInputStream方法会返回FileInputStream类,这个类可以将普通的文件转换成byte数组。
ZLZipEntryFile类的getInputStream方法会返回FileInputStream类,这个类可以将epub内部压缩过的xml文件转换成可以正常解析的byte数组
下面看一下AndroidAssetsFile的getInputStream方法。可以猜测,读取帮助文档的时候调用getInputStream会返回这个文件的InputStream
1 AndroidAssetsFile:: getInputStream()
1 2 3 4 @Override public InputStream getInputStream () throws IOException { return myApplication.getAssets().open(getPath()); }
得到ZLResourceFile 对象之后,我们回到FBReaderApp::openBook 这个方法中。 可以看到通过
1 book = Collection.getBookByFile(BookUtil.getHelpFile());
将ZLResourceFile对象转成了Book 对象了。 Collection是一个接口IBookCollection, 这里是BookCollectionShadow实现了这个接口,在FBReaderApp的onCreate方法,我们可以看到这句
BookCollectionShadow又是什么呢?我们还要往下分析
上面的代码中创建了一个FBReaderApp对象, 至于这个对象是干什么的,现在还不知道。 接下来,getCollection 并 调用了 bindToService方法
1 FBReader::getCollection()
1 2 3 private BookCollectionShadow getCollection () { return (BookCollectionShadow) myFBReaderApp.Collection; }
调用的正是这个主activity的一个方法。返回的对象是FBReaderApp 的 Collection变量,这个正式刚才创建的BookCollectionShadow 实现了IBookCollection接口。
接着调用了BookCollectionShadow的bindToService方法
1 BookCollectionShadow::bindToService(Context context, Runnable onBindAction)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public synchronized void bindToService(Context context , Runnable onBindAction) { if (myInterface != null && myContext == context ) { if (onBindAction != null ) { Config.Instance ().runOnConnect (onBindAction); } } else { if (onBindAction != null ) { myOnBindActions.add (onBindAction); } context .bindService ( FBReaderIntents.internalIntent (FBReaderIntents.Action .LIBRARY_SERVICE ), this , LibraryService.BIND_AUTO_CREATE ); myContext = context ; } }
这里执行了一句很熟悉的context.bindService 方法,这里用到了aidl。这方面的问题就不记录了,跟主向无关。 bindService方法有三个参数,第二个参数传了BookCollectionShadow本身, 我们知道,bindService的第二个参数传的是ServiceConnection接口,在这里面, 我们可以调用aidl文件生命的方法,进行跨进程的通讯。也不知道fbreader为什么弄这么多进程是想干什么。
果然BookCollectionShadow实现了ServiceConnection,并且myInterface真是那个aidl的全局变量, 我们可以通过它,完成与服务端的沟通。
四、怎样获得book对象 经过一下午的瞎看,大致的熟悉了一下fbreader的源码。
带着问题学习总是最快的,那么,fbreader到底是怎样解析epub文件的呢。我们得找一个入口。在项目中,长按菜单键,会弹出一个功能列表,会看到一个本地书柜,一顿操作之后,我们可以找到一个我事先导入的一个电子书
最后我们会来到这个界面
在茫茫码海中怎么找到这个activity,用这样一条命令
1 adb shell dumpsys activity top | grep ACTIVITY --color
在这个activitiy中, mybook是通过intent传入的,所以找上个页面
1 2 3 4  ListActivity是什么? 没用过,TreeActivity自己写的, 太复杂。想着在LibraryActivity中能找到一些方法
LibraryActivity::onListItemClick(ListView listView, View view, int position, long rowId)
1 2 3 4 5 6 7 8 9 10 11 12 ``` @Override protected void onListItemClick (ListView listView, View view, int position, long rowId) { final LibraryTree tree = (LibraryTree)getListAdapter().getItem(position); final Book book = tree.getBook(); LogUtils.d("LibraryActivity -> onListItemClick book: " + book); if (book != null ) { showBookInfo(book); } else { openTree(tree); } }
在这个方法中, 是通过
1 2 3 4 ``` public Book getBook() { return null; }
这尼玛返回空!!! tree是LibraryTree的一个实例,在TreeAdapter的getItem(int position) 方法中得到的
1 2 3 public FBTree getItem(int position ) { return myItems.get (position ); }
存放在了叫
final List myItems;```这样的一个list之中。 1 2 3 4 5 6 7 8 9 10 11 12 13 一路尾随,不是跟踪myItems, 看一下TreeAdapter的replaceAll方法  根据名字我们能判断。得到现在树的子树,然后添加到myItem中去的。那么我们知道现在的树, 或者现在树的子树是什么,应该就可以得到答案了。 现在的树集成关系是这样的  又在onListItemClick方法中强装成LibraryTree, 我们只需关注LibraryTree的之类就行了 这个树通过getTreeByKey得到
LibraryActivity::getTreeByKey(FBTree.Key key)
1 2 3 4 5 6 ``` @Override protected LibraryTree getTreeByKey (FBTree.Key key) { return key != null ? myRootTree.getLibraryTree(key) : myRootTree; } private synchronized void deleteRootTree () {
最后我们要找的这棵树实际跟
1 myRootTree在onCreate的时候创建
LibraryActivity::onCreate(Bundle icicle)
@Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); Log.d(TAG, “start onCreate function: “); mySelectedBook = FBReaderIntents.getBookExtra(getIntent()); new LibraryTreeAdapter(this); getListView().setTextFilterEnabled(true); getListView().setOnCreateContextMenuListener(this); deleteRootTree(); myCollection.bindToService(this, new Runnable() { public void run() { setProgressBarIndeterminateVisibility(!myCollection.status().IsCompleted); myRootTree = new RootTree(myCollection); myCollection.addListener(LibraryActivity.this); init(getIntent()); } }); }
1 2 3 4 5 6 在倒数第二行add的接口的回掉在这里  { return myLibrary; }
1 2 3 4 5 6 7 8 9 10 11 12 13 返回的真是aidl 的一个接口 ![] (http : 以上是接口创建的一部分代码 .runOnConnect不知道是干嘛用的,反正是调用了以下的方法  { public void onReceive(Context context, Intent intent) { if (!hasListeners()) { return; } try { final String type = intent.getStringExtra(“type”); LogUtils.d(“BookCollectionShadow -> onReceive type: “ + type); if (LibraryService.BOOK_EVENT_ACTION.equals(intent.getAction())) { final Book book = SerializerUtil.deserializeBook(intent.getStringExtra(“book”)); fireBookEvent(BookEvent.valueOf(type), book); } else { fireBuildEvent(Status.valueOf(type)); } } catch (Exception e) { // ignore } } };
1 2 3 4 5 6 7 8 接下来看log  怎么返回空? 经过上面的弯路, 我们知道了最后回掉的是```LibraryActivity:: onBookEvent(BookEvent event, Book book)```这个方法
@Override public void onBookEvent(BookEvent event, Book book) { if (getCurrentTree().onBookEvent(event, book)) { getListAdapter().replaceAll(getCurrentTree().subtrees(), true); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 再跟一下里面的方法 ```java public boolean onBookEvent(BookEvent event, Book book) { switch (event) { default: case Added: return false ; case Removed: return removeBook(book); case Updated: { boolean changed = false ; for (FBTree tree : this ) { if (tree instanceof BookTree) { final Book b = ((BookTree)tree).Book; if (b.equals(book)) { b.updateFrom(book); changed = true ; } } } return changed; } } }
我们看到了booktree这个东西,原来我们正经使用的是booktree的getBook,所以get得到的肯定是一本书
这里留一个问题, 这个booktree是怎么来的? 先不着急分析他 我们发现book 是从booktree 得到的, 而booktree中的book 是构造是传进来的。
而这些又是在FilteredTree的抽象发方法createSubtree, 得到的
接着往上看FilteredTree这个类
终于找到主进程里的book了, 原来是调用进程里的 Collection.books(query);方法, 来获得一个book的列表。
五、怎么跳转到Fbreader这个activity 本来想看看怎么解析epub的,但是感觉目前还消化不了。
我们要把这个完整的项目当成一个sdk来使用,虽然说整个加到工程中fbreader的5M左右了,但是没有办法,时间紧任务重。我倒是很赞同自己去写个阅读器,但是条件不允许。
我们要使用这个项目, 就得找到一个书, 然后跳转到这个activity让他显示。
经过以上的分析, 可以看到,在查找书的时候 已经就装成book对象了。反正要是我写,我肯定在查找文件的时候返回的url, 然后根据这个去解析成book的对象。
记得最早之前分析过,在FBReader的onResume是启动的关键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private synchronized void openBook(Intent intent, final Runnable action, boolean force) { if (!force && myBook != null ) { return ; } myBook = FBReaderIntents.getBookExtra(intent); final Bookmark bookmark = FBReaderIntents.getBookmarkExtra(intent); LogUtils.d("FBReader -> openBook myBook: " + myBook); if (myBook == null ) { final Uri data = intent.getData(); LogUtils.d("FBReader -> openBook data: " + data); if (data != null ) { myBook = createBookForFile(ZLFile.createFileByPath(data.getPath())); } } if (myBook != null ) { ZLFile file = myBook.File ; LogUtils.d("FBReader -> openBook file path: " + file .getPath()); LogUtils.d("FBReader -> openBook file exists: " + file .exists()); if (!file .exists()) { if (file .getPhysicalFile() != null ) { file = file .getPhysicalFile(); } UIUtil.showErrorMessage(this , "fileNotFound" , file .getPath()); myBook = null ; } }
这里,但书为空的时时候,在intent.getData去 拿到url, 传到ZLFile里去。也就是说,我在start这个activity 传一个url 进去, 于是我这样写了一段
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void openBookActivity (Context context, String path) { final Intent intent = new Intent(context, FBReader.class) .setAction(FBReaderIntents.Action.VIEW) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.setData(Uri.parse(path)); context.startActivity(intent); }
这样就可以了。 我翻了几页之后, 默认就会保存阅读的位置。那么我想重新阅读这个文章应该怎么办。继续撸代码
在
book, Bookmark bookmark, boolean force)```方法中,第一次打开书是会执行```gotoStoredPosition();```这样一个方法,从字面的意思是去到存储的位置
FBReaderApp::gotoStoredPosition()
1 2 3 4 5 6 7 8 9 10 ``` private void gotoStoredPosition () { myStoredPositionBook = Model != null ? Model.Book : null ; if (myStoredPositionBook == null ) { return ; } myStoredPosition = getStoredPosition(myStoredPositionBook); BookTextView.gotoPosition(myStoredPosition); savePosition(); }
是调用了BookTextView的gotoPosition这个方法。那我们看看能否在FBReader这个类上搞点事情。在openBook方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 private synchronized void openBook(Intent intent, final Runnable action, boolean force) { if (!force && myBook != null ) { return ; } myBook = FBReaderIntents.getBookExtra(intent); final Bookmark bookmark = FBReaderIntents.getBookmarkExtra(intent); LogUtils.d("FBReader -> openBook myBook: " + myBook); if (myBook == null ) { final Uri data = intent.getData(); LogUtils.d("FBReader -> openBook data: " + data); if (data != null ) { myBook = createBookForFile(ZLFile.createFileByPath(data.getPath())); } } LogUtils.d("FBReader -> openBook mybook: " + myBook); if (myBook != null ) { ZLFile file = myBook.File ; LogUtils.d("FBReader -> openBook file path: " + file .getPath()); LogUtils.d("FBReader -> openBook file exists: " + file .exists()); if (!file .exists()) { if (file .getPhysicalFile() != null ) { file = file .getPhysicalFile(); } UIUtil.showErrorMessage(this , "fileNotFound" , file .getPath()); myBook = null ; } } Config.Instance().runOnConnect(new Runnable() { public void run() { LogUtils.d("FBReader -> openBook run thread: " + Thread.currentThread()); LogUtils.d("FBReader -> openBook run myBook: " + myBook); LogUtils.d("FBReader -> openBook run bookmark: " + bookmark); LogUtils.d("FBReader -> openBook run action: " + action); myFBReaderApp.openBook(myBook, bookmark, action); myFBReaderApp.BookTextView.gotoHome(); } }); }
在最后一句,这个activity中可以拿到myFBReaderApp,猜想是用来控制整个阅读器的类, 调用gotohome, 竟然可以了。通过这个就可以控制是否继续阅读还是从头开始
六、跳转到固定章节 一本书有很多章节,跳转到固定章节的时候不可能进行一步步的翻页操作。碰巧fbreader提供这样的功能,而且还有快速翻看
找打开一本书之后Model 字段就会赋值, ‘弹幕’一下这个字段, 看到里面确实存在了章节的信息
我知道了章节,然后怎么去跳转,我们看下面这一段代码:
1 TOCActivity::openBookText(TOCTree tree)
1 2 3 4 5 6 7 8 9 10 11 void openBookText(TOCTree tree) { final TOCTree.Reference reference = tree.getReference() if (reference != null) { finish() final FBReaderApp fbreader = (FBReaderApp)ZLApplication.Instance(); fbreader.addInvisibleBookmark(); fbreader.BookTextView.gotoPosition(reference.ParagraphIndex, 0 , 0 ) fbreader.showBookTextView(); fbreader.storePosition() } }
得到reference对象的ParagraphIndex, 然后去调用BookTextView的gotoPosition
在FBReaderApp中这样写试试
这样是可以达到预期效果的,但是有一定要值得注意: 在第一次读取书时,获取书的操作是个异步的, 也就是说,这个时候Model可能为空,所以在以后开发中,最好是用接口,将获取书的情况反到activity中, 这样,当书加载完成时再去做相应的跳转操作。
七、字体加大与缩小 源码中,改变字体大小的就在菜单的按键中
点击按键监听再这里,这么搞也是特殊,从未见过啊,不知道干嘛弄的这么复杂。像这样可以实现这个功能了。
这样只是增加与减少,万一需求上是给定几个固定的字号,然后调节怎么办?所以开始得看看源码
跟进去发现,最终的action是存在于这个map, 实在主activity创建时put的,所以所有的操作都会交给ZLAction的子类去处理
也就是上面选中的这个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class ChangeFontSizeAction extends FBAction { private final int myDelta; ChangeFontSizeAction (FBReaderApp fbreader, int delta) { super (fbreader); myDelta = delta; } @Override protected void run(Object ... params) { final ZLIntegerRangeOption option = Reader .ViewOptions .getTextStyleCollection().getBaseStyle().FontSizeOption ; option.setValue(option.getValue() + myDelta); LogUtils .d("ChangeFontSizeAction -> run: " + option.getValue()); Reader .clearTextCaches(); Reader .getViewWidget().repaint(); } }
执行run方法后会设置字号,这里的option.setValue(option.getValue() + myDelta);就是对字号的设置。要是愿意的话可以复写FBAction或者直接使用run方法中的参数进行传值,当然,后一种要好一点。
八、音量键功能 一个功能完整的阅读器,音量键也都会派上用场。fbreader音量键也不例外
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ZLAndroidWidget::@Override public boolean onKeyDown(int keyCode , KeyEvent event) { final ZLApplication application = ZLApplication.Instance(); final ZLKeyBindings bindings = application.keyBindings(); if (bindings.hasBinding(keyCode , true ) || bindings.hasBinding(keyCode , false )) { if (myKeyUnderTracking != -1 ) { if (myKeyUnderTracking == keyCode ) { return true ; } else { myKeyUnderTracking = -1 ; } } if (bindings.hasBinding(keyCode , true )) { myKeyUnderTracking = keyCode ; myTrackingStartTime = System.currentTimeMillis(); return true ; } else { return application.runActionByKey(keyCode , false ); } } else { return false ; } }
1 2 3 4 5 6 7 8 public final boolean runActionByKey(int key , boolean longPress) { final String actionId = keyBindings().getBinding(key , longPress); if (actionId != null ) { final ZLAction action = myIdToActionMap.get (actionId); return action != null && action.checkAndRun(); } return false ; }
程序在runActionByKey方法中控制这按键
key 是当前案件的键码,会对所有按键处理,如果你接键盘的话。 longPress 字面意思是是否长按,但是我试过永远的短按,永远的false
以后处理案件就可在这里处理,或者深入到action里面进行处理。
九、更换背景,字体颜色 更换背景以及字体颜色,也是实现夜间模式的一个套路。首先我们定位到PreferenceActivity,冷不丁一看,我去,这不是系统里的settings嘛。这么长的init至于么?
这么长的代码就不全粘贴了,大概在400多行,有这么一段。是添加背景和墙纸的 我们跟到这个类中BackgroundPreference
在onBindView下面是跳转到颜色选择器的代码,在PreferenceActivity中的onActivityResult方法中返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data ) { if (myNetworkContext.onActivityResult(requestCode, resultCode, data )) { return ; } if (resultCode != RESULT_OK) { return ; } if (BACKGROUND_REQUEST_CODE == requestCode) { if (myBackgroundPreference != null ) { myBackgroundPreference.update(data ); } return ; } myChooserCollection.update(requestCode, data ); }
中间部分, 调用了yBackgroundPreference.update(data);
1 2 3 4 5 6 7 8 9 10 11 12 13 public void update(Intent data) { final String value = data.getStringExtra(VALUE_KEY); LogUtils.d("BackgroundPreference -> update: " + value); if (value != null ) { myProfile.WallpaperOption.setValue(value); } final int color = data.getIntExtra(COLOR_KEY, -1 ); LogUtils.d("BackgroundPreference -> update: " + color ); if (color != -1 ) { myProfile.BackgroundOption.setValue(new ZLColor(color )); } notifyChanged(); }
继续往里跟,方向设置颜色或者是壁纸图片, 只是设置了WallpaperOption的value。我们能在ColorProfile类中找到这些参数,那么我们怎么在主activity获取并且改变它呢
还是最重要的
的实例 ViewOptions,通过它我们就能拿到ColorProfile, 图下设置背景为红色 1 2 ``` myFBReaderApp.ViewOptions.getColorProfile().BackgroundOption.setValue(new ZLColor(255, 0, 0));
但是直接这么写,没有变化,仔细想想。我只设置了颜色,但是没有通知重绘,自然就没有变化。
那么,颜色选择怎么通知到这个activity的呢,只能是Intent传的,我们看下FBReader 的onActivityResult(int requestCode, int resultCode, Intent data)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override protected void onActivityResult (int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_PREFERENCES: if (resultCode != RESULT_DO_NOTHING && data != null ) { final Book book = FBReaderIntents.getBookExtra(data); if (book != null ) { getCollection().bindToService(this , new Runnable() { public void run () { onPreferencesUpdate(book); } }); } } break ; case REQUEST_CANCEL_MENU: runCancelAction(data); break ; } }
ok, 确实有我需要的东西,这个方法调用了onPreferencesUpdate
1 2 3 4 private void onPreferencesUpdate(Book book) { AndroidFontUtil.clearFontCache(); myFBReaderApp.onBookUpdated(book); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \ public void onBookUpdated(Book book) { if (Model == null || Model.Book == null || !Model.Book.equals(book)) { return ; } final String new Encoding = book.getEncodingNoDetection(); final String oldEncoding = Model.Book.getEncodingNoDetection(); Model.Book.updateFrom(book); if (new Encoding != null && !new Encoding .equals(oldEncoding)) { reloadBook(); } else { ZLTextHyphenator.Instance().load(Model.Book.getLanguage()); clearTextCaches(); getViewWidget().repaint(); } }
起到决定性作用的就行最后一句 getViewWidget().repaint();
那么,更改背景颜色就是 (上面的代码都是等书加载完毕之后,显示在view中在去设置的, 不然书都没加载完,自然也就设置不了)
BackgroundOption 是背景色 RegularTextOption 是文字的颜色
十、动画类型 下面开始研究应用的翻页动画。 我们修改颜色实际上是修改了ZLAndroidWidget。我们可以跟进这个类看一下代码
1 ZLAndroidWidget::getAnimationProvider()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private AnimationProvider getAnimationProvider() { final ZLView .Animation type = ZLApplication .Instance ().getCurrentView() .getAnimationType(); if (myAnimationProvider == null || myAnimationType != type ) { myAnimationType = type ; switch (type ) { case none: myAnimationProvider = new NoneAnimationProvider (myBitmapManager); break ; case curl: myAnimationProvider = new CurlAnimationProvider (myBitmapManager); break ; case slide: myAnimationProvider = new SlideAnimationProvider ( myBitmapManager); break ; case shift: myAnimationProvider = new ShiftAnimationProvider ( myBitmapManager); break ; case left2right: myAnimationProvider = new Left2RightAnimationProvider ( myBitmapManager); break ; case simulation: myAnimationProvider = new EmulateAnimationProvider ( myBitmapManager); break ; } } return myAnimationProvider; }
代码跟你的不一样,正常,这个我改过了。 在绘制动画的时候,也就是onDrawInScrolling(Canvas canvas)这个方法被调用的时候,都会获取一下当前的动画。
那么在设置动画的时候调用的是PreferenceActivity这个activity,再init一堆东西里看以看到,这样一句话
原来是改变的是pageTurningOptions这个东西,我们再看主activity中能不能找到这个对象。回到FBReader当中。我们可以这样设置动画
1 myFBReaderApp .PageTurningOptions .Animation .setValue (ZLView .Animation .left2right );
因为是执行每一次翻页的动作都会get一下当前的动画,所以也就不需要重绘当前页面,写这样一句就好。
十一、点击区域 市场上的阅读类应用。基本都是点击左面上一页,右面下一页。中间会弹出一个设置菜单。 我发现我的这个version的代码,点击中间是没有任何反应的,所以。继续撸码。
关于点击时间, 首先去看ZLAndroidWidget的onTouchEvent方法
1 ZLAndroidWidget::onTouchEvent(MotionEvent event)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 @Override public boolean onTouchEvent(MotionEvent event) { int x = (int ) event.getX(); int y = (int ) event.getY(); final ZLView view = ZLApplication.Instance().getCurrentView(); switch (event.getAction()) { case MotionEvent.ACTION_UP: if (myPendingDoubleTap) { view.onFingerDoubleTap(x, y); } else if (myLongClickPerformed) { 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 { view.onFingerSingleTap(x, y); } } else { view.onFingerRelease(x, y); } } myPendingDoubleTap = false ; myPendingPress = false ; myScreenIsTouched = false ; break ; case MotionEvent.ACTION_DOWN: if (myPendingShortClickRunnable != null ) { removeCallbacks(myPendingShortClickRunnable); myPendingShortClickRunnable = null ; myPendingDoubleTap = true ; } else { postLongClickRunnable(); myPendingPress = true ; } myScreenIsTouched = true ; myPressedX = x; myPressedY = y; break ; case MotionEvent.ACTION_MOVE: { final int slop = ViewConfiguration.get(getContext()) .getScaledTouchSlop(); final boolean isAMove = Math.abs(myPressedX - x) > slop || Math.abs(myPressedY - y) > slop; if (isAMove) { myPendingDoubleTap = false ; } if (myLongClickPerformed) { view.onFingerMoveAfterLongPress(x, y); } else { if (myPendingPress) { if (isAMove) { if (myPendingShortClickRunnable != null ) { removeCallbacks(myPendingShortClickRunnable); myPendingShortClickRunnable = null ; } if (myPendingLongClickRunnable != null ) { removeCallbacks(myPendingLongClickRunnable); } view.onFingerPress(myPressedX, myPressedY); myPendingPress = false ; } } if (!myPendingPress) { view.onFingerMove(x, y); } } break ; } } return true ; }
当按键抬起的时候, 将手指的位置传给了 view.onFingerSingleTap(x, y); view就是FBView这个类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Override public boolean onFingerSingleTap (int x, int y) { if (super .onFingerSingleTap(x, y)) { return true ; } final ZLTextRegion hyperlinkRegion = findRegion(x, y, MAX_SELECTION_DISTANCE, ZLTextRegion.HyperlinkFilter); if (hyperlinkRegion != null ) { selectRegion(hyperlinkRegion); myReader.getViewWidget().reset(); myReader.getViewWidget().repaint(); myReader.runAction(ActionCode.PROCESS_HYPERLINK); return true ; } final ZLTextRegion videoRegion = findRegion(x, y, 0 , ZLTextRegion.VideoFilter); if (videoRegion != null ) { selectRegion(videoRegion); myReader.getViewWidget().reset(); myReader.getViewWidget().repaint(); myReader.runAction(ActionCode.OPEN_VIDEO, (ZLTextVideoRegionSoul) videoRegion.getSoul()); return true ; } final ZLTextHighlighting highlighting = findHighlighting(x, y, MAX_SELECTION_DISTANCE); if (highlighting instanceof BookmarkHighlighting) { myReader.runAction( ActionCode.SELECTION_BOOKMARK, ((BookmarkHighlighting) highlighting).Bookmark ); return true ; } String actionId = getZoneMap() .getActionByCoordinates(x, y, getContextWidth(), getContextHeight(), isDoubleTapSupported() ? TapZoneMap.Tap.singleNotDoubleTap : TapZoneMap.Tap.singleTap); myReader.runAction(actionId, x, y); return true ; }
这段的最后一句就是或坐标的区域
一路跟下去, 发现
这样一个类,看它的构造 1 2 3 4 5 6 7 8 9 10 11 12 13 ``` private TapZoneMap(String name) { Name = name; myOptionGroupName = "TapZones:" + name; LogUtils.d("TapZoneMap -> TapZoneMap: " + name); myHeight = new ZLIntegerRangeOption(myOptionGroupName, "Height", 2, 5, 3); myWidth = new ZLIntegerRangeOption(myOptionGroupName, "Width", 2, 5, 3); final ZLFile mapFile = ZLFile.createFileByPath( "default/tapzones/" + name.toLowerCase() + ".xml" ); new Reader().readQuietly(mapFile); }
是读了一个文件,我们在资源文件中找下
果然在这里躺着一堆的文件,应该是按照屏幕的方向 选择默认的配置文件,这里默认的就是right_to_left.xml,我们打开看看。
1 2 3 4 5 6 7 8 9 10 11 <tapZones v ="3" h ="3" > <zone x ="0" y ="0" action ="previousPage" action2 ="navigate" /> <zone x ="0" y ="1" action ="previousPage" /> <zone x ="0" y ="2" action ="previousPage" action2 ="menu" /> <zone x ="1" y ="0" action2 ="navigate" /> <zone x ="1" y ="1" action2 ="menu" /> <zone x ="1" y ="2" action2 ="menu" /> <zone x ="2" y ="0" action ="nextPage" action2 ="navigate" /> <zone x ="2" y ="1" action ="nextPage" /> <zone x ="2" y ="2" action ="nextPage" action2 ="menu" /> </tapZones >
猜测一下,fbreader应该是把屏幕分成3 x 3的区域,大概就是上面图片的意思。用这个坐标代表,我要点击屏幕中间的,自然我就加了一个1,1的坐标。
得到区域后,执行了
myReader.runAction(actionId, x, y);``` 这个方法。 1 跟音量键的一样,他会把事件分发的```ShowMenuAction```这个类里面。
class ShowMenuAction extends FBAndroidAction { ShowMenuAction(FBReader baseActivity, FBReaderApp fbreader) { super(baseActivity, fbreader); }
@Override
protected void run(Object ... params) {
BaseActivity.openOptionsMenu();
//BaseActivity.menu();
}
}`
源码里调用的是openOptionsMenu,这个baseactivity就是我们的FBReader这个类。我改成自己的方法, 就可以定制自己的菜单栏了,然后抛弃源码提供的菜单栏。
ps:代码看到这里差不多可以进行定制了, 但是,代码里确实是有些无用的东西, 要是能把这些东西去掉的话,做一下精简。应该会更好。