FBReader 源碼閱讀筆記(一)

FBREADER 源碼閱讀筆記

前言

這篇文章是我在讀源碼時候的筆記。是我的一個習慣吧!在閱讀源碼的時候會記錄一下思路属提,省得自己會忘記,相當于“保護現場”了吧美尸。由于是一邊看代碼一邊記錄冤议,一定會有很多的錯誤,請大家見諒师坎。

一恕酸、代碼導入

https://github.com/geometer/FBReaderJ 這個地址上就是fbreader的java項目。

版本庫上面的項目是eclipse編寫的胯陋,所以第一步蕊温,想辦法把這個項目變成AS上開發(fā)的

(因為種種原因,我并沒有clone github上的源碼遏乔,在我們svn存在著之前的一個fbreader版本义矛,是2.0的)

1、目的

導入fbreader這個項目是想在自己的程序中加入閱讀器的功能按灶。但從頭開始開發(fā)時間長症革,并且沒做過。所以參考了fbreader鸯旁,在源碼的基礎上做二次開發(fā)噪矛。

代碼同步下來之后,發(fā)現铺罢。fbreader并不是一個開源庫(SDK),而是一個完整的項目艇挨,部分功能使用了jni開發(fā),并且支持插件化韭赘,通過aidl缩滨,進行組件之間的通訊

因為要導入現有的項目(重構ing),想法是將fbreader整個項目編譯成aar泉瞻,然后導入現有項目脉漏。因為時間短,起初可以先把整個fbreader導入袖牙,之后對fbreader研究之后侧巨,或去掉相應模塊或者重新開發(fā)

2、導入

導入過程比較繁瑣鞭达,又沒什么技術含量司忱。主要就是將之前ant構建的項目換成gradle構建皇忿。

這里設計jni和aidl的目錄結構有所變化,按照android studio上面的結構統(tǒng)一創(chuàng)建就好

(這里我犯了一個錯誤坦仍,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 都是空的,根本不執(zhí)行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代碼尿孔,發(fā)現執(zhí)行了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輸出白指,這三個參數都是空的。執(zhí)行了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);
    }

三個參數酵紫,大體上能猜測出是什么意思告嘲,但是,并不是很清晰奖地。
執(zhí)行到getCurrentServerBook一句時橄唬,但我們第一次啟動應用是,此時的book對象是空参歹,即使是getCurrentServerBook執(zhí)行完之后還是空的仰楚。之后便去找到這個幫助文檔getHelpFile。 然后轉化成book對象犬庇。
之后僧界,貌似創(chuàng)建了線程。

SynchronousExecutor這個東西是個接口由ZLApplication:: createExecutor(String key) 創(chuàng)建

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線程中創(chuàng)建一個對話框
                        myProgress = ProgressDialog.show(activity, null, myMessage, true, false);
                        // 在線程中執(zhí)行第一個參數
                        final Thread runner = new Thread() {
                            public void run() {
                                // 在線程中運行第一個參數笆豁,也就是打開圖書(在)
                                action.run();
                                // 執(zhí)行完之后,關閉這個對話框
                                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方法赤赊,這里先顯示一個進度框闯狱,然后執(zhí)行第一個參數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 {
            // 創(chuàng)建一個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
        }
    }

目前能讀懂的都在注釋上器躏,貌似在執(zhí)行 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);
    }

這里,通過文件的路徑醒陆,創(chuàng)建了一個AndroidAssetsFile瀑构。AndroidAssetsFile繼承了ZLResourceFile,是ZLFile的子類刨摩。

ZLFile 是fbreader對所有文件的同意描述检碗。上圖是繼承樹。

以下是從網絡上摘取的資料

  • ResourceFile類專門用來處理資源文件码邻,這一章中要解析的assets文件夾下的資源文件都可以ZLResourceFile類來處理

  • ZLResourceFile類專門用來處理資源文件折剃,這一章中要解析的assets文件夾下的資源文件都可以ZLResourceFile類來處理。

  • ZLPhysicalFile類專門用來處理普通文件像屋,eoub文件就可以用一個ZLPhysicalFile類來代表怕犁。

  • ZLZipEntryFile類用來處理epub文件內部的xml文件,這個類會在第五章“epub文件處理 -- 解壓epub文件”中出現己莺。

這三個文件類都實現了getInputStream抽象方法奏甫,不用的文件類會通過這個方法獲得針對當前文件類的字節(jié)流類。

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又是什么呢局蚀?我們還要往下分析


上面的代碼中創(chuàng)建了一個FBReaderApp對象, 至于這個對象是干什么的恕稠,現在還不知道至会。
接下來,getCollection 并 調用了 bindToService方法

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

調用的正是這個主activity的一個方法谱俭。返回的對象是FBReaderApp 的 Collection變量,這個正式剛才創(chuàng)建的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;
        }
    }

這里執(zhí)行了一句很熟悉的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;
}

這尼瑪返回空!B馈几于!
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的時候創(chuàng)建

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的一個接口

以上是接口創(chuàng)建的一部分代碼

在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是怎么來的站辉? 先不著急分析他


我們發(fā)現book 是從booktree 得到的, 而booktree中的book 是構造是傳進來的损姜。

而這些又是在FilteredTree的抽象發(fā)方法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);
}

這樣就可以了。 我翻了幾頁之后落剪, 默認就會保存閱讀的位置睁本。那么我想重新閱讀這個文章應該怎么辦。繼續(xù)擼代碼

FBReaderApp::openBookInternal(Book book, Bookmark bookmark, boolean force)方法中忠怖,第一次打開書是會執(zhí)行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严卖, 竟然可以了席舍。通過這個就可以控制是否繼續(xù)閱讀還是從頭開始

六布轿、跳轉到固定章節(jié)

一本書有很多章節(jié)哮笆,跳轉到固定章節(jié)的時候不可能進行一步步的翻頁操作。碰巧fbreader提供這樣的功能汰扭,而且還有快速翻看

找打開一本書之后Model 字段就會賦值稠肘, ‘彈幕’一下這個字段, 看到里面確實存在了章節(jié)的信息

Paste_Image.png

我知道了章節(jié)萝毛,然后怎么去跳轉项阴,我們看下面這一段代碼:

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可能為空歉胶,所以在以后開發(fā)中,最好是用接口巴粪,將獲取書的情況反到activity中通今, 這樣粥谬,當書加載完成時再去做相應的跳轉操作。

七辫塌、字體加大與縮小

源碼中漏策,改變字體大小的就在菜單的按鍵中

點擊按鍵監(jiān)聽再這里,這么搞也是特殊臼氨,從未見過啊掺喻,不知道干嘛弄的這么復雜。像這樣可以實現這個功能了一也。

這樣只是增加與減少巢寡,萬一需求上是給定幾個固定的字號,然后調節(jié)怎么辦椰苟?所以開始得看看源碼

跟進去發(fā)現抑月,最終的action是存在于這個map, 實在主activity創(chuàng)建時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();
    }
}

執(zhí)行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,冷不丁一看伞访,我去掂骏,這不是系統(tǒng)里的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();
}

繼續(xù)往里跟,方向設置顏色或者是壁紙圖片辛馆, 只是設置了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);

因為是執(zhí)行每一次翻頁的動作都會get一下當前的動畫魂贬,所以也就不需要重繪當前頁面,寫這樣一句就好裙顽。

十一付燥、點擊區(qū)域

市場上的閱讀類應用∮蹋基本都是點擊左面上一頁键科,右面下一頁。中間會彈出一個設置菜單漩怎。
我發(fā)現我的這個version的代碼勋颖,點擊中間是沒有任何反應的,所以勋锤。繼續(xù)擼碼饭玲。

關于點擊時間, 首先去看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;
}

這段的最后一句就是或坐標的區(qū)域

一路跟下去, 發(fā)現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的區(qū)域灵巧,大概就是上面圖片的意思屯换。用這個坐標代表,我要點擊屏幕中間的,自然我就加了一個1串塑,1的坐標。

得到區(qū)域后靖苇,執(zhí)行了FBView:: myReader.runAction(actionId, x, y); 這個方法阱州。
跟音量鍵的一樣,他會把事件分發(fā)的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閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件踪栋,死亡現場離奇詭異,居然都是意外死亡图毕,警方通過查閱死者的電腦和手機夷都,發(fā)現死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來予颤,“玉大人囤官,你說我怎么就攤上這事「蚺埃” “怎么了党饮?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長驳庭。 經常有香客問我刑顺,道長,這世上最難降的妖魔是什么饲常? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任蹲堂,我火速辦了婚禮,結果婚禮上贝淤,老公的妹妹穿的比我還像新娘柒竞。我一直安慰自己,他們只是感情好播聪,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布朽基。 她就那樣靜靜地躺著,像睡著了一般犬耻。 火紅的嫁衣襯著肌膚如雪踩晶。 梳的紋絲不亂的頭發(fā)上执泰,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天枕磁,我揣著相機與錄音,去河邊找鬼术吝。 笑死计济,一個胖子當著我的面吹牛茸苇,可吹牛的內容都是我干的。 我是一名探鬼主播沦寂,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼学密,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了传藏?” 一聲冷哼從身側響起腻暮,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毯侦,沒想到半個月后哭靖,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡侈离,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年试幽,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卦碾。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡铺坞,死狀恐怖,靈堂內的尸體忽然破棺而出洲胖,到底是詐尸還是另有隱情济榨,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布绿映,位于F島的核電站腿短,受9級特大地震影響,放射性物質發(fā)生泄漏绘梦。R本人自食惡果不足惜橘忱,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卸奉。 院中可真熱鬧钝诚,春花似錦、人聲如沸榄棵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疹鳄。三九已至拧略,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瘪弓,已是汗流浹背垫蛆。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人袱饭。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓川无,卻偏偏與公主長得像,于是被迫代替她去往敵國和親虑乖。 傳聞我的和親對象是個殘疾皇子懦趋,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,079評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現疹味,斷路器仅叫,智...
    卡卡羅2017閱讀 134,652評論 18 139
  • SQL: Create/Read/Update/DeleteBrowse/Read/Edit/Add/Delete...
    幻灰龍閱讀 304評論 0 1
  • 很長一段時間以來继找,我一直封閉著自己遂跟,直到今天我跟久未謀面的好友聊天才發(fā)現。 最近為了方便聯系婴渡,建了一個我和...
    巧笑_倩兮_閱讀 442評論 0 1
  • 一次沒認真幻锁,未登上高等學府的門 又一次沒認真,這輩子成為他的婦人 陰霾日子里曾有或多或少的不甘心 怎能就此消磨殆盡...
    冰紗伊人A閱讀 159評論 0 0