FBReader 源码阅读笔记(一)

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
@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);
        // setting keyboard default mode
        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()中了

FBReader::onResume()
    @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) {
            // it's maybe run here
            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 这个是什么? 先不管它,往下看

接下来调用了

FBReader::openBook(Intent intent, final Runnable action, boolean force)
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;
            }
        }


        // 打开app 时 正常myBook为空 intent.getData 为空
        /*
        * 在主线程运行
        *
        * 正常打开时myBook, bookmark, action 三个参数都是空
        * */
        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方法

FBReaderApp::(Book book, final Bookmark bookmark, Runnable postAction)
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()) {
                // get helpfile
                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) 创建

ZLApplication:: createExecutor(String key)
 protected SynchronousExecutor createExecutor(String key) {
        if (myWindow != null) {
            return myWindow.createExecutor(key);
        } else {
            return myDummyExecutor;
        }
    }

这里调用了myWindow的createExecutor方法,myWindow(ZLApplicationWindow)是 一个接口,FBReader实现了这个接口。

接着,调用了UIUtil的createExecutor方法

UIUtil::createExecutor(final Activity activity, final String key)
    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() {
                        // 在ui线程中创建一个对话框
                        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)中的

FBReaderApp::openBookInternal(bookToOpen, bookmark, false);

    /**
     * 打开内部的图书
     */
    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();

        // 猜测是根据book,加载一个用来读取这个book的插件
        final FormatPlugin plugin = book.getPluginOrNull();
        // 此时,阅读默认帮助文档时,插件为fb2
        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 {
            // 创建一个BookModel, 通过判断插件的type
            Model = BookModel.createModel(book);
            // BookCollectionShadow 暂时不懂
            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) {
            // ignore
        }
    }

目前能读懂的都在注释上,貌似在执行 setView(BookTextView)时,就会进行渲染的操作了

把帮助文档当成图书的话, 第一次出现对书的解析应该就是在BookUtil的getHelpFile的方法中

BookUtil::getHelpFile()
public static ZLResourceFile getHelpFile() {
        final Locale locale = Locale.getDefault();
        // 获取local,取得帮助文档
        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

ZLResourceFile :: createResourceFile(String path)
    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中进行了初始化操作。

ZLAndroidLibrary::createResourceFile(String path)
  @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

AndroidAssetsFile:: getInputStream()
 @Override
public InputStream getInputStream() throws IOException {
     return myApplication.getAssets().open(getPath());
}

得到ZLResourceFile 对象之后,我们回到FBReaderApp::openBook 这个方法中。
可以看到通过 book = Collection.getBookByFile(BookUtil.getHelpFile());
将ZLResourceFile对象转成了Book 对象了。
Collection是一个接口IBookCollection, 这里是BookCollectionShadow实现了这个接口,在FBReaderApp的onCreate方法,我们可以看到这句

BookCollectionShadow又是什么呢?我们还要往下分析


上面的代码中创建了一个FBReaderApp对象, 至于这个对象是干什么的,现在还不知道。
接下来,getCollection 并 调用了 bindToService方法

FBReader::getCollection()
private BookCollectionShadow getCollection() {
        return (BookCollectionShadow) myFBReaderApp.Collection;
    }

调用的正是这个主activity的一个方法。返回的对象是FBReaderApp 的 Collection变量,这个正式刚才创建的BookCollectionShadow 实现了IBookCollection接口。

接着调用了BookCollectionShadow的bindToService方法

BookCollectionShadow::bindToService(Context context, Runnable onBindAction)
    public synchronized void bindToService(Context context, Runnable onBindAction) {
        if (myInterface != null && myContext == context) {
            // not first connect
            if (onBindAction != null) {
                Config.Instance().runOnConnect(onBindAction);
            }
        } else {
            // first connect
            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,用这样一条命令

adb shell dumpsys activity top | grep ACTIVITY --color

在这个activitiy中, mybook是通过intent传入的,所以找上个页面LibraryActivity

ListActivity是什么? 没用过,TreeActivity自己写的, 太复杂。想着在LibraryActivity中能找到一些方法

LibraryActivity::onListItemClick(ListView listView, View view, int position, long rowId)
@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);
    }
}

在这个方法中, 是通过tree.getBook();得到的一个book,点进去看了一下

public Book getBook() {
    return null;
}

这尼玛返回空!!!
tree是LibraryTree的一个实例,在TreeAdapter的getItem(int position) 方法中得到的

    public FBTree getItem(int position) {
        return myItems.get(position);
    }

存放在了叫private final List<FBTree> myItems;这样的一个list之中。

一路尾随,不是跟踪myItems, 看一下TreeAdapter的replaceAll方法

根据名字我们能判断。得到现在树的子树,然后添加到myItem中去的。那么我们知道现在的树, 或者现在树的子树是什么,应该就可以得到答案了。

现在的树集成关系是这样的


又在onListItemClick方法中强装成LibraryTree, 我们只需关注LibraryTree的之类就行了

这个树通过getTreeByKey得到

LibraryActivity::getTreeByKey(FBTree.Key key)
@Override
protected LibraryTree getTreeByKey(FBTree.Key key) {
    return key != null ? myRootTree.getLibraryTree(key) : myRootTree;
}
private synchronized void deleteRootTree() {

最后我们要找的这棵树实际跟myRootTree有关。
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());
        }
    });
}

在倒数第二行add的接口的回掉在这里


onBookEvent中一定是这个树的来源,看来我们要去另一个进程里看看了

这个服务和之前的一样LibraryService。我们跟进LibraryService

@Override
public IBinder onBind(Intent intent) {
    return myLibrary;
}

返回的真是aidl的一个接口

以上是接口创建的一部分代码

在reset中搞了一个Config.Instance().runOnConnect不知道是干嘛用的,反正是调用了以下的方法

我注意到底下的两个广播,很明显是做进程间通讯。然后, 我在BookCollectionShadow中找到了这个广播的接收者

private final BroadcastReceiver myReceiver = new BroadcastReceiver() {
    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
        }
    }
};

接下来看log


通过这个<entry xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata"> 搞成一本书???

这个先别管,我的疑惑是tree.getBook() 怎么返回空?

经过上面的弯路, 我们知道了最后回掉的是LibraryActivity:: onBookEvent(BookEvent event, Book book)这个方法

@Override
public void onBookEvent(BookEvent event, Book book) {
    if (getCurrentTree().onBookEvent(event, book)) {
        getListAdapter().replaceAll(getCurrentTree().subtrees(), true);
    }
}

再跟一下里面的方法

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是启动的关键

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 进去, 于是我这样写了一段

/**
 * 打开一本电子书
 *
 * @param context 上下文
 * @param path    epub 资源的绝对路径
 */
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);
}

这样就可以了。 我翻了几页之后, 默认就会保存阅读的位置。那么我想重新阅读这个文章应该怎么办。继续撸代码

FBReaderApp::openBookInternal(Book book, Bookmark bookmark, boolean force)方法中,第一次打开书是会执行gotoStoredPosition();这样一个方法,从字面的意思是去到存储的位置

具体的方法是这样的

FBReaderApp::gotoStoredPosition()
private void gotoStoredPosition() {
    myStoredPositionBook = Model != null ? Model.Book : null;
    if (myStoredPositionBook == null) {
        return;
    }
    myStoredPosition = getStoredPosition(myStoredPositionBook);
    BookTextView.gotoPosition(myStoredPosition);
    savePosition();
}

是调用了BookTextView的gotoPosition这个方法。那我们看看能否在FBReader这个类上搞点事情。在openBook方法中

 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;
            }
        }


        // 打开app 时 正常myBook为空 intent.getData 为空
        /*
        * 在主线程运行
        *
        * 正常打开时myBook, bookmark, action 三个参数都是空
        * */
        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);
                // // TODO: 6/7/2017 回到首页
                myFBReaderApp.BookTextView.gotoHome();
            }
        });
    }

在最后一句,这个activity中可以拿到myFBReaderApp,猜想是用来控制整个阅读器的类, 调用gotohome, 竟然可以了。通过这个就可以控制是否继续阅读还是从头开始

六、跳转到固定章节

一本书有很多章节,跳转到固定章节的时候不可能进行一步步的翻页操作。碰巧fbreader提供这样的功能,而且还有快速翻看

找打开一本书之后Model 字段就会赋值, ‘弹幕’一下这个字段, 看到里面确实存在了章节的信息

Paste_Image.png

我知道了章节,然后怎么去跳转,我们看下面这一段代码:

TOCActivity::openBookText(TOCTree tree) 
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的子类去处理


也就是上面选中的这个类

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音量键也不例外

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;
        }
    }
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方法中返回

@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);

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获取并且改变它呢

还是最重要的FBReaderApp里面有ViewOptions 的实例 ViewOptions,通过它我们就能拿到ColorProfile, 图下设置背景为红色

 myFBReaderApp.ViewOptions.getColorProfile().BackgroundOption.setValue(new ZLColor(255, 0, 0));

但是直接这么写,没有变化,仔细想想。我只设置了颜色,但是没有通知重绘,自然就没有变化。

那么,颜色选择怎么通知到这个activity的呢,只能是Intent传的,我们看下FBReader 的onActivityResult(int requestCode, int resultCode, Intent data)

@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

   private void onPreferencesUpdate(Book book) {
        AndroidFontUtil.clearFontCache();
        myFBReaderApp.onBookUpdated(book);
    }
\    public void onBookUpdated(Book book) {
        if (Model == null || Model.Book == null || !Model.Book.equals(book)) {
            return;
        }

        final String newEncoding = book.getEncodingNoDetection();
        final String oldEncoding = Model.Book.getEncodingNoDetection();

        Model.Book.updateFrom(book);

        if (newEncoding != null && !newEncoding.equals(oldEncoding)) {
            reloadBook();
        } else {
            ZLTextHyphenator.Instance().load(Model.Book.getLanguage());
            clearTextCaches();
            getViewWidget().repaint();
        }
    }

起到决定性作用的就行最后一句 getViewWidget().repaint();

那么,更改背景颜色就是
(上面的代码都是等书加载完毕之后,显示在view中在去设置的, 不然书都没加载完,自然也就设置不了)


BackgroundOption 是背景色
RegularTextOption 是文字的颜色

十、动画类型

下面开始研究应用的翻页动画。
我们修改颜色实际上是修改了ZLAndroidWidget。我们可以跟进这个类看一下代码

ZLAndroidWidget::getAnimationProvider()
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 SimulateAnimationProvider(
                // myBitmapManager);
                myAnimationProvider = new EmulateAnimationProvider(
                        myBitmapManager);
                break;
            }
        }
        return myAnimationProvider;
    }

代码跟你的不一样,正常,这个我改过了。
在绘制动画的时候,也就是onDrawInScrolling(Canvas canvas)这个方法被调用的时候,都会获取一下当前的动画。

那么在设置动画的时候调用的是PreferenceActivity这个activity,再init一堆东西里看以看到,这样一句话

原来是改变的是pageTurningOptions这个东西,我们再看主activity中能不能找到这个对象。回到FBReader当中。我们可以这样设置动画

 myFBReaderApp.PageTurningOptions.Animation.setValue(ZLView.Animation.left2right);

因为是执行每一次翻页的动作都会get一下当前的动画,所以也就不需要重绘当前页面,写这样一句就好。

十一、点击区域

市场上的阅读类应用。基本都是点击左面上一页,右面下一页。中间会弹出一个设置菜单。
我发现我的这个version的代码,点击中间是没有任何反应的,所以。继续撸码。

关于点击时间, 首先去看ZLAndroidWidget的onTouchEvent方法

ZLAndroidWidget::onTouchEvent(MotionEvent event)
@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这个类。

@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) {
        // click link
        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) {
        // click video
        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;
}

这段的最后一句就是或坐标的区域

一路跟下去, 发现TapZoneMap 这样一个类,看它的构造

    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,我们打开看看。

<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的坐标。

得到区域后,执行了FBView:: myReader.runAction(actionId, x, y); 这个方法。
跟音量键的一样,他会把事件分发的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:代码看到这里差不多可以进行定制了, 但是,代码里确实是有些无用的东西, 要是能把这些东西去掉的话,做一下精简。应该会更好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,368评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • SQL: Create/Read/Update/DeleteBrowse/Read/Edit/Add/Delete...
    幻灰龙阅读 301评论 0 1
  • 很长一段时间以来,我一直封闭着自己,直到今天我跟久未谋面的好友聊天才发现。 最近为了方便联系,建了一个我和...
    巧笑_倩兮_阅读 428评论 0 1
  • 一次没认真,未登上高等学府的门 又一次没认真,这辈子成为他的妇人 阴霾日子里曾有或多或少的不甘心 怎能就此消磨殆尽...
    冰纱伊人A阅读 156评论 0 0