前言
看了整整一个月的FBReader的代码及文章,终于搞懂了一点怎么使用FBReader,现在我向大家分享下怎么使用和修改FBReader,这个是面向小白的,毕竟当初的我看这个阅读器的时候也有点迷茫。第一次写简书,写得不好请见谅。
在看怎么使用前,先感谢 初见破晓 大佬的 FBReader 源码阅读笔记(一),里面给我们讲解了部分FBReader的源代码,推荐要用这个FBReader的大家先去看一看(虽然说是大佬是讲解了,但是代码还是要自己看的,不要指望不读源代码就能了解怎么改)
修改的内容
我的FBReader
是基于https://github.com/adolfAn/FBReader_AS修改而来的,做了以下修改
1、添加了菜单界面
2、添加了右侧目录界面
3、添加了txt
目录解析
4、合并了FBReader
的库(未精简)
经测试,支持txt、epub、mobi
等格式,至于pdf
,大家可以去看看MuPDF
或者PDFView
,整个FBReader
用的还是几年前开源的,听说现在的FBReader
已经不开源了,郁闷。
目录
- 为自己的项目导入
FBReader
-
FBReader
的常识 - 怎么使用
FBReaderHelper
- 自定义
FBReader
1、怎么为自己的项目导入FBReader
(1)首先,先下载我的FBReader
项目,要加FBReader
库,肯定要知道项目库到底能不能跑对吧。 我的环境是:Android Studio 3.3.2
。
(2)如果能跑了,那就说明你的开发环境是可以的咯,然后把整个fBReader
库导入到你自己的项目中,怎么导入呢,这就要大家自己自行百度了。
(3)添加读写权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
(4)接着在自己的主项目中创建Application
类文件,并在onCreate
中对FBReader
进行初始化,并修改static
静态域中的字段,填入你的 applicationId
。这里的 applicationId
是你的App项目(主项目)build.gradle
文件里的 applicationId
字段的值。如果 FBReaderIntents.DEFAULT_PACKAGE
字段不正确,结果会是打开书本后什么都没有,一片空白。
public class App extends Application {
static {
//这里需要自己设置自己 build.gradle 里的 applicationId 到DEFAULT_PACKAGE字段
FBReaderIntents.DEFAULT_PACKAGE = "你应用的applicationId";
}
@Override
public void onCreate() {
super.onCreate();
FBReaderApplication.init(this);
}
}
(5)初始化工作完成后,接着就是打开书本了,参考提供的 MainActivity.java
文件,复制粘贴 onResume
和 onPause
函数中的操作作至你的项目Activity
中,再赋值onClick
事件函数中的语句,该语句为打开FBReader
的核心语句。
public class MainActivity extends AppCompatActivity {
private FBReaderHelper fbReaderHelper;
// private String path = Environment.getExternalStorageDirectory() + "/test.txt";
// private String path2 = Environment.getExternalStorageDirectory() + "/test.mobi";
private String path3 = Environment.getExternalStorageDirectory() + "/test.epub";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fbReaderHelper = new FBReaderHelper(this);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//必须确保activity有绑定服务才能通过jni获取书本信息
fbReaderHelper.bindToService(new Runnable() {
@Override
public void run() {
Book book = fbReaderHelper.getCollection().getBookByFile(path3);
FBReader.openBook(MainActivity.this, book, null);
}
});
}
});
}
@Override
protected void onResume() {
super.onResume();
//对fbreader阅读服务进行绑定
fbReaderHelper.bindToService(null);
}
@Override
protected void onPause() {
//注销fbreader阅读服务绑定,service只允许绑定一个activity,所以为保证下一个activity能使用阅读服务,必须注销
fbReaderHelper.unBind();
super.onPause();
}
}
(6)运行
2、FBReader的常识
在讲使用 FBReaderHelper
前先补充下 FBReader
的常识:
(1)FBReader
在你打开了一本书后,会缓存当前的这本书,尽管你关闭了FBReader
,由于静态缓存,书本仍然被保留下来,所以你下次打开书本将会是秒开的,只有你打开另一本书的时候书本缓存才会被覆盖。
(2)FBReader
对书本的读取需要通过JNI
交互 ,同时他的数据持久化存储(存进数据库)也通过JNI
,所以看不懂书籍解析和数据存储的并且看不懂C++
的朋友就有。。。,没关系的,因为,我也看不懂。
(3)FBReader
对图片和其他资源文件的读取都是异步加载的,所以当你要获取图片的时候肯定需要异步回调才能读取到。
(4)FBReader
的中文排版可能有点丑,因为它并不是中国的,所以你看中文的时候可能会觉得有点怪。
(5)FBReader
对文本的读取和显示是以段落划分的,FBReader
对字符的定位有3个参数:
public final int ParagraphIndex; //段落索引
public final int ElementIndex; //词的索引
public final int CharIndex; //字母的索引
由于英文单词是由字母组成的,所有会出现字母的索引,但是我们中文是没有的,所以当你使用中文的时候,字母的索引为 0 。FBReader
需要这3个参数才能定位到文本的位置。
(6)FBReader
的事件机制信息传递是使用其自己建立的Action来实现的,就是一种观察者模式,他显示通过绑定Action事件,例如下面的显示目录事件,关于他在哪里发起事件的,大家可以追踪下ActionCode.SHOW_TOC
字段。
myFBReaderApp.addAction(ActionCode.SHOW_TOC, new ShowTOCAction(this, myFBReaderApp));
(7)FBReader
的架构有点老,可能看起来会有点吃力,但是其中也是有可以参考的代码结构的。
(8)在使用FBReader
的时候如果你找不到某个功能在哪个代码里,我可以多用用AS的全局搜索快捷键,记得灵活使用。
3、怎么使用FBReaderHelper
说了那么多,你只是教了我们怎么导入使用怎么还不给我们解释FBReaderHelper
是啥,我™。别急,我现在讲,放下你的板凳。
FBReaderHelper
类顾名思义,就是方便我们对 FBReader
进行操作的帮助类,里面几乎所有方法都是对 FBReader
的操作,但其作用不单止在于帮助我们操作,它还能帮助我们理解FBReader
的源码,毕竟里面的方法就是从FBReader
里面找出来的。
我把对FBReader
的操作理解为3个状态:未读取、预读取、完全读取。
未读取:顾名思义
预读取:就是只通过 FBReader
获取了书本的Book
类和 BookModel
类,但是没有打开该书本,没有进入到读书界面。
完全读取:说多了就是,把整本书打开了,连书本有什么字,有什么图片都显示了出来了。
(1)像设置字体大小这种阅读配置能在3种状态下进行,因为该设置并不一定需要 Book
、BookModel
。
/**
* 设置字体大小
*/
public void setFontSize(int size) {
myFBReaderApp.ViewOptions.getTextStyleCollection().getBaseStyle().FontSizeOption.setValue(size);
if (myFBReaderApp.getViewWidget() != null) {
myFBReaderApp.clearTextCaches();
myFBReaderApp.getViewWidget().repaint();
}
}
(2)但是像获取书本总字数这种就必须先让Activity
绑定FBReader
的 service
才能使用了,因为书本的预读取是需要通过 Service
和 JNI
交互的,从service
处获取到书本的BookModel
后才能获取书本的总字数和段落数。
/**
* 获取书本总字数 应该先确定Collection是否已经绑定服务且已打开书本(完全读取)
*
* @return
*/
public int getSumTextCount() {
if (myFBReaderApp.Model == null) return 0;
else {
return myFBReaderApp.Model.getTextModel().getTextLength(
myFBReaderApp.Model.getTextModel().getParagraphsNumber()
);
}
}
//也可以这样实现(传入一个Book对象进行预读取获取BookModel从而获取字体总数)(预读取)
public int getSumTextCount(Book book) {
BookModel bookModel = createBookModel(book);
return bookModel.getTextModel().getTextLength(
bookModel.getTextModel().getParagraphsNumber()
);
}
两者的出来的结果都是一样的,区别在于,一个是完全读取书籍,一个是预读取。
(3)完全读取需要FBReader
打开整本书籍,走完打开书本的流程,这样可以获取到书本的所有信息,包括文字图片,例如获取当前页字数就需要读取书籍并显示后才能知道当前页的字数。
/**
* 获取当前页的字数量 应该先确定Collection是否已经绑定服务且已打开书本
*
* @return
*/
public int getCurPageWordCount() {
ZLTextWordCursor stCursor = getStartCursor();
ZLTextWordCursor edCursor = getEndCursor();
if (myFBReaderApp.Model == null || edCursor.getParagraphIndex() <= 0)
return edCursor.getElementIndex();
return myFBReaderApp.Model.getTextModel().getTextLength(edCursor.getParagraphIndex() - 1) + edCursor.getElementIndex() -
myFBReaderApp.Model.getTextModel().getTextLength(stCursor.getParagraphIndex()) + stCursor.getElementIndex();
}
(4)图片的加载,在FBReader
中图片的加载时异步的,所以当你需要获取图片的时候肯定是需要回调的,例如封面图的获取。
/**
* 异步加载封面图
*
* @param book FBReader 的 Book 对象
* @param listener
*/
public void loadBookCover(@NonNull Book book, @NonNull final OnGetCoverListener listener) {
PluginCollection pluginCollection = PluginCollection.Instance(Paths.systemInfo(activity));
BookUtil.getEncoding(book, pluginCollection);
final ZLImage image = CoverUtil.getCover(book, pluginCollection);
if (image == null) {
listener.finish(false, null);
} else {
if (image instanceof ZLImageProxy) {
((ZLImageProxy) image).startSynchronization(new AndroidImageSynchronizer(activity), new Runnable() {
public void run() {
loadCover(image, listener);
}
});
} else {
loadCover(image, listener);
}
}
}
(5)是否需要预读取或完全读取呢,我已经在 FBReaderHelper
的函数上写上注释了,大家在使用的时候记得区分下,要不然可能会报空指针或者获取到的信息是上一本打开过的书籍的。
(6)可能FBReaderHelper
会有点坑,例如空指针的问题,这个嘛是因为我当时写出来只是为了记录操作方法,如果说所获取到的 myFBReaderApp.Model
为空,你或许需要自己调用 FBReaderHelper.createBookModel()
来代替。
4、自定义FBReader
- 修改图片显示的配置
- 自定义图片的打开方式
- 自定义底部时间栏的显示
- 翻页状态的监听
- 设置默认配置
- 拦截跳转到下一章
4.1. 修改图片显示的配置
图片配置的文件在org.geometerplus.fbreader.fbreader.options.ImageOptions.java
文件中。
public ImageOptions() {
ImageViewBackground =
new ZLColorOption("Colors", "ImageViewBackground", new ZLColor(255, 255, 255));
FitToScreen =
new ZLEnumOption<FBView.ImageFitting>("Options", "FitImagesToScreen", FBView.ImageFitting.covers);
TapAction =
new ZLEnumOption<TapActionEnum>("Options", "ImageTappingAction", TapActionEnum.openImageView);
MatchBackground =
new ZLBooleanOption("Colors", "ImageMatchBackground", false);
}
从上往下数,四个对象分别控制的是图片的背景色,图片的填充缩放类型,图片点击后的行为,图片的遮罩效果。
主要讲的是第二个对象,如果设置的值是FBView.ImageFitting.covers
,则是只有封面图填充整个阅读器可视范围。如果设置的是FBView.ImageFitting.all
则是所有图都填充整个阅读器可视范围。
如果想实现点击图片后不对图片处理或打开可以将TapActionEnum.openImageView
换成TapActionEnum.doNothing
。
4.2. 自定义图片的打开方式
图片视图是一个Activity
界面,叫org.geometerplus.android.fbreader.image.ImageViewActivity.java
,当在阅读界面点击图片时,会调用打开该Activity的代码
} else if (soul instanceof ZLTextImageRegionSoul) {
Reader.getTextView().hideOutline();
Reader.getViewWidget().repaint();
final String url = ((ZLTextImageRegionSoul) soul).ImageElement.URL;
if (url != null) {
try {
final Intent intent = new Intent();
intent.setClass(BaseActivity,ImageViewActivity.class);
intent.putExtra(ImageViewActivity.URL_KEY, url);
intent.putExtra(
ImageViewActivity.BACKGROUND_COLOR_KEY,
Reader.ImageOptions.ImageViewBackground.getValue().intValue()
);
OrientationUtil.startActivity(BaseActivity, intent);
BaseActivity.overridePendingTransition(R.anim.activity_anim_no, R.anim.activity_anim_no);
} catch (Exception e) {
e.printStackTrace();
}
}
}
只需要修改参考Activity
里面的代码编写即可。
4.3. 自定义底部时间栏的显示
FBReader
默认时间栏和阅读进度显示在底部的,如果需要显示在上方需要修改改动的地方比较多,这里只介绍下如何修改底部栏显示的样式。
控制底部栏显示的具体代码在 org.geometerplus.fbreader.fbreader.FBView.java
文件中,你需要做的是修改其大概位于642行的FooterNewStyle
类中的paint()
函数。相信看到这个函数内容的人心里或多或少都会有些B数,知道该怎么修改了,在这里我贴下代码。
private class FooterNewStyle extends Footer {
public synchronized void paint(ZLPaintContext context) {
final FooterOptions footerOptions = myViewOptions.getFooterOptions();
final ColorProfile cProfile = myViewOptions.getColorProfile();
context.clear(cProfile.FooterNGBackgroundOption.getValue());
context.clear(getBackgroundColor());
final BookModel model = myReader.Model;
if (model == null) return;
final int left = getLeftMargin();
final int right = context.getWidth() - getRightMargin();
final int height = getHeight();
final int charHeight = setFont(context, height / 2, false);
final PagePosition pagePosition = FBView.this.pagePosition();
context.setTextColor(cProfile.RegularTextOption.getValue());
if (footerOptions.ShowBattery.getValue()) {
context.drawString(left, (height + charHeight + 1) / 2, "电池 " + myReader.getBatteryLevel() + "%");
}
if (footerOptions.showProgressAsPages()) {
String str = pagePosition.Current + "/" + pagePosition.Total;
int strWidth = context.getStringWidth(str);
context.drawString((right - strWidth+left) / 2 , (height + charHeight + 1) / 2, str);
}
if (footerOptions.showProgressAsPercentage() && pagePosition.Total != 0) {
String str = pagePosition.Current + "/" + pagePosition.Total + " " + 100 * pagePosition.Current / pagePosition.Total + "%";
int strWidth = context.getStringWidth(str);
context.drawString((right - strWidth+left) / 2 , (height + charHeight + 1) / 2, str);
}
if (footerOptions.ShowClock.getValue()) {
String str = ZLibrary.Instance().getCurrentTimeString();
int strWidth = context.getStringWidth(str);
context.drawString(right - strWidth, (height + charHeight + 1) / 2, str);
}
}
}
实际起效的代码在21行以后,是不是很像我们用过的canvas
画板和paint
画笔?
4.4. 翻页状态的监听
我在org.geometerplus.fbreader.fbreader
包下添加了3个Action
,分别是PageTurnStartAction
、PageTurningAction
、PageTurnEndAction
,如果FBReader
发生了翻页,那么会分别顺序执行三个Action,如果你想计算翻页后的页面的字体数量,那么可以在PageTurnEndAction
的run()
函数中编写获取字数的代码。注意:没有翻页动画的时候是不会执行PageTurningAction
的。
4.5. 设置默认配置
在org.geometerplus.zlibrary.ui.android.library.ZLAndroidLibrary.java
中保存着一些应用级别的设置。
public final ZLBooleanOption ShowStatusBarOption = new ZLBooleanOption("LookNFeel", "ShowStatusBar", false);
public final ZLBooleanOption OldShowActionBarOption = new ZLBooleanOption("LookNFeel", "ShowActionBar", false);
public final ZLBooleanOption ShowActionBarOption = new ZLBooleanOption("LookNFeel", "ShowActionBarNew", false);
public final ZLBooleanOption EnableFullscreenModeOption = new ZLBooleanOption("LookNFeel", "FullscreenMode", true);
public final ZLBooleanOption DisableButtonLightsOption = new ZLBooleanOption("LookNFeel", "DisableButtonLights", !DeviceType.Instance().hasButtonLightsBug());
public final ZLIntegerRangeOption BatteryLevelToTurnScreenOffOption = new ZLIntegerRangeOption("LookNFeel", "BatteryLevelToTurnScreenOff", 0, 100, 50);
public final ZLBooleanOption DontTurnScreenOffDuringChargingOption = new ZLBooleanOption("LookNFeel", "DontTurnScreenOffDuringCharging", true);
public final ZLIntegerRangeOption ScreenBrightnessLevelOption = new ZLIntegerRangeOption("LookNFeel", "ScreenBrightnessLevel", 0, 100, 0);
这些英文都是比较好看懂的,如果不懂得可以google下。
4.6. 拦截跳转到下一章
对于FBReader_AS
的源码我稍微修改了下,把PageTurnAction
翻页行为的注册挪了下位置,并在里面添加了翻页拦截,当每进行翻页时会执行FBReader.java
文件最底部的onTurnBackIntercept
方法
public boolean onTurnBackIntercept(boolean isForward) {
//判断是否是准备翻到下一页的并且当前页是该章节的最后一页
if (isForward && myFBReaderApp.BookTextView.getEndCursor().getParagraphCursor().isEndOfSection()) {
//TODO 准备翻到下一章的拦截如果需要拦截返回true
return false;
}
return false;
}
这个函数传入一个参数isForward
,如果为true
,就是往右翻页,false
就为左翻页。
该函数的返回值的类似于View
中的onTouchEvent
,如果返回值为true
,阅读器就不会进行翻页,大家可以在return
前做自己的事情,例如弹出一个请求框之类的,如果返回值为false
,阅读器就会正常的翻页,不停止翻页。
先写着这么多吧,其实说多了,如果要改fbreader,真的必须看懂源码,这个文章就当作给写引子吧,灵活一点,多用用ctrl+左键和全局搜索,几乎都能找到你想要找的东西。