FBReader如何读取电子书内容,以及页面绘制的方式是什么
先来回顾一下上一节最后说到的点,新角色FBReaderApp调用了openBookInternal方法:
private synchronized void openBookInternal(final Book book, Bookmark bookmark, boolean force) {
//忽略部分代码..
Model = BookModel.createModel(book, plugin);
Collection.saveBook(book);
ZLTextHyphenator.Instance().load(book.getLanguage());
BookTextView.setModel(Model.getTextModel());
//忽略部分代码..
}
复制代码
一、BookModle生成过程中,都有哪些“不为人知的秘密”
上一篇,我们已经分析过,在BookModel.createModel生成BookModel时,针对于epub格式的文件来说,最终会调用NativeFormatPlugin的readModelNative:
private native int readModelNative(BookModel model, String cacheDir);
复制代码
这里有两个参数BookModel和cacheDir,我们先来看看BookModel是怎么生成的:
public static BookModel createModel(Book book, FormatPlugin plugin) throws BookReadingException {
if (plugin instanceof BuiltinFormatPlugin) {
final BookModel model = new BookModel(book);
((BuiltinFormatPlugin)plugin).readModel(model);
return model;
}
//忽略部分代码..
}
复制代码
直接new BookModel,并且将book装入。再来看看cacheDir:
String tempDirectory = SystemInfo.tempDirectory();
复制代码
这个SystemInfo上一篇我们已经分析过,其实现为Paths.systemInfo(context)。是用来获取一些路径地址的。那么这里传入的路径是什么?debug看一下:
传入了一个路径给native,取名cache,看来navtive在解析电子书时会生成缓存?暂时把这个疑问放一边,去看一下BookModel这个类:
有好多方法都是灰色的,证明在java代码中没有地方调用这些代码,细看一下,这些都是一些set赋值操作,不免想到是否在native进行解析时会调用呢?经过debug后发现,的确在navtive解析电子书时,会调用这些操作赋值许多数据,这也解释了上一篇最后关于BookModel解析前后内容存在差别的原因。这里有三个方法,值得我们去关注一下:
1.initInternalHyperlinks——生成BookModel对应的存储管理CachedCharStorage
public void initInternalHyperlinks(String directoryName, String fileExtension, int blocksNumber) {
myInternalHyperlinks = new CachedCharStorage(directoryName, fileExtension, blocksNumber);
}
CachedCharStorage.class
public CachedCharStorage(String directoryName, String fileExtension, int blocksNumber) {
myDirectoryName = directoryName + '/';
myFileExtension = '.' + fileExtension;
myArray.addAll(Collections.nCopies(blocksNumber, new WeakReference<char[]>(null)));
}
复制代码
参数名称很清楚,文件目录、文件扩展名和blocksNumber。CachedCharStorage在构建时,会根据传入的blocksNumber创建一个大小为blocksNumber集合,其它的暂时看来还不清楚有什么用。debug看一下initInternalHyperlinks被调用时具体参数传递情况:
很明显了,这个文件路径跟我们之前传递进去的路径是一个路径,文件扩展名是nlinks。看来native不只是解析,还会在解析的过程中生成缓存文件,而且缓存文件的存放地址就是我们传入的地址。
2.createTextModel——初始化核心类ZLTextPlainModel
public ZLTextModel createTextModel(
String id, String language, int paragraphsNumber,
int[] entryIndices, int[] entryOffsets,
int[] paragraphLenghts, int[] textSizes, byte[] paragraphKinds,
String directoryName, String fileExtension, int blocksNumber
) {
return new ZLTextPlainModel(
id, language, paragraphsNumber,
entryIndices, entryOffsets,
paragraphLenghts, textSizes, paragraphKinds,
directoryName, fileExtension, blocksNumber, myImageMap, FontManager
);
}
ZLTextPlainModel.class
public ZLTextPlainModel(
String id,
String language,
int paragraphsNumber,
int[] entryIndices,
int[] entryOffsets,
int[] paragraphLengths,
int[] textSizes,
byte[] paragraphKinds,
String directoryName,
String fileExtension,
int blocksNumber,
Map<String,ZLImage> imageMap,
FontManager fontManager
) {
myId = id;
myLanguage = language;
myParagraphsNumber = paragraphsNumber;
myStartEntryIndices = entryIndices;
myStartEntryOffsets = entryOffsets;
myParagraphLengths = paragraphLengths;
myTextSizes = textSizes;
myParagraphKinds = paragraphKinds;
myStorage = new CachedCharStorage(directoryName, fileExtension, blocksNumber);
myImageMap = imageMap;
myFontManager = fontManager;
}
复制代码
这个参数个数就很多了,而且有些参数并不能看出来是做什么的。但是不难发现这里也有这么三个参数:directoryName,fileExtension,blocksNumber。那么这三个参数实际值又是什么呢?还得需要debug看一下:
地址还是我们传入的地址,但是这里文件类型变成了ncache,而且blocksNumber是12,我们知道CachedCharStorage会对应的创建一个长度为12的集合。
3.调用BookModel的setBookTextModel,将2创建的ZLTextPlainModel赋值给BookModel
public void setBookTextModel(ZLTextModel model) {
myBookTextModel = model;
}
复制代码
这里debug可以知道,将第二步创建的ZLTextPlainModel赋值给了BookModel。
回到FBReaderApp的openBookInternal方法,我们将断点放在BookModel.createModel之后的Collection.saveBook(book),当断点到这里时,进入手机,我们去看一下刚才路径下面是否有我们之前猜测的native生成的缓存文件:
果然!这里有.ncache和.nlinke文件,而且个数分别为12和1。跟blocksNumber大小一致。
大胆的猜测一下,这个ncache是不是在native解析内容时,每达到一定大小(图中128K)就会切分出来一个缓存文件,然后根据某些条件去读取对应的缓存文件中的内容?
二、获取页面对应Bitmap并绘制到cavas上
在之前查看FBReader的布局文件时,我们知道,其页面中只有一个控件——ZLAndroidWidget。既然要看绘制,那不多说直入onDraw:
@Override
protected void onDraw(final Canvas canvas) {
final Context context = getContext();
if (context instanceof FBReader) {
//唤醒屏幕
((FBReader)context).createWakeLock();
} else {
System.err.println("A surprise: view's context is not an FBReader");
}
super.onDraw(canvas);
//final int w = getWidth();
//final int h = getMainAreaHeight();
myBitmapManager.setSize(getWidth(), getMainAreaHeight());
if (getAnimationProvider().inProgress()) {
onDrawInScrolling(canvas);
} else {
onDrawStatic(canvas);
ZLApplication.Instance().onRepaintFinished();
}
}
复制代码
这里引出了myBitmapManager,看一下它是什么,在哪定义的:
原来是BitmapManagerImpl,那就看一下setSize,是做啥了:
private final int SIZE = 2;
private final Bitmap[] myBitmaps = new Bitmap[SIZE];
void setSize(int w, int h) {
if (myWidth != w || myHeight != h) {
myWidth = w;
myHeight = h;
for (int i = 0; i < SIZE; ++i) {
myBitmaps[i] = null;
myIndexes[i] = null;
}
System.gc();
System.gc();
System.gc();
}
}
复制代码
很简单,判断、赋值和清空bitmap集合。之前传递过来的参数第一个是getWidth即为当前控件的宽度,但是第二个参数缺不是getHeight而是getMainAreaHeight:
private int getMainAreaHeight() {
final ZLView.FooterArea footer = ZLApplication.Instance().getCurrentView().getFooterArea();
return footer != null ? getHeight() - footer.getHeight() : getHeight();
}
复制代码
这里信息量比较大,我们分开来一个一个的看:
1.ZLApplication.Instance() 在FBReader的onCreate中我们已经分析过,是FBReaderApp实例
2.getCurrentView(),经过追溯能够知道实际为FBView对象。
public final ZLView getCurrentView() {
return myView;
}
赋值方法
protected final void setView(ZLView view) {
if (view != null) {
myView = view;
//忽略部分代码...
}
}
FBReaderApp.class
public FBReaderApp(SystemInfo systemInfo, final IBookCollection<Book> collection) {
super(systemInfo);
//忽略部分代码...
BookTextView = new FBView(this);
}
private synchronized void openBookInternal(final Book book, Bookmark bookmark, boolean force) {
//忽略部分代码...
setView(BookTextView);
//忽略部分代码...
}
复制代码
3.getFooterArea:
FBView.class
@Override
public Footer getFooterArea() {
//根据ViewOptions中定义的footer类型,创建相应的footer
switch (myViewOptions.ScrollbarType.getValue()) {
case SCROLLBAR_SHOW_AS_FOOTER:
if (!(myFooter instanceof FooterNewStyle)) {
if (myFooter != null) {
myReader.removeTimerTask(myFooter.UpdateTask);
}
myFooter = new FooterNewStyle();
myReader.addTimerTask(myFooter.UpdateTask, 15000);
}
break;
case SCROLLBAR_SHOW_AS_FOOTER_OLD_STYLE:
if (!(myFooter instanceof FooterOldStyle)) {
if (myFooter != null) {
myReader.removeTimerTask(myFooter.UpdateTask);
}
myFooter = new FooterOldStyle();
myReader.addTimerTask(myFooter.UpdateTask, 15000);
}
break;
default:
if (myFooter != null) {
myReader.removeTimerTask(myFooter.UpdateTask);
myFooter = null;
}
break;
}
return myFooter;
}
private abstract class Footer implements FooterArea {
//忽略部分代码...
public int getHeight() {
//返回ViewOptions中设置的footer高度
return myViewOptions.FooterHeight.getValue();
}
//忽略部分代码...
}
复制代码
经过上面三步的分析,可以得出的结论是getMainAreaHeight方法获取到的高度是ZLAndroidWidget的高度减去Footer的高度。那么也就是说BitmapManager在创建bitmap时,的最大高度为去掉Footer区域后的高度:
public Bitmap getBitmap(ZLView.PageIndex index) {
//忽略部分代码...
myBitmaps[iIndex] = Bitmap.createBitmap(myWidth, myHeight, Bitmap.Config.RGB_565);
//忽略部分代码...
}
复制代码
我们再回到onDraw中,可以看到其中有一个判断:
if (getAnimationProvider().inProgress()) {
onDrawInScrolling(canvas);
} else {
onDrawStatic(canvas);
ZLApplication.Instance().onRepaintFinished();
}
//获取当前翻页动画
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 slideOldStyle:
myAnimationProvider = new SlideOldStyleAnimationProvider(myBitmapManager);
break;
case shift:
myAnimationProvider = new ShiftAnimationProvider(myBitmapManager);
break;
}
}
return myAnimationProvider;
}
复制代码
那么就是当翻页动画正在执行的时候,绘制调用onDrawInScrolling,如果动画没在执行,说明当前是静止的状态,绘制调用onDrawStatic。这里我们先看onDrawStatic:
public final ExecutorService PrepareService = Executors.newSingleThreadExecutor();
private void onDrawStatic(final Canvas canvas) {
canvas.drawBitmap(myBitmapManager.getBitmap(ZLView.PageIndex.current), 0, 0, myPaint);
drawFooter(canvas, null);
post(new Runnable() {
public void run() {
PrepareService.execute(new Runnable() {
public void run() {
final ZLView view = ZLApplication.Instance().getCurrentView();
final ZLAndroidPaintContext context = new ZLAndroidPaintContext(
mySystemInfo,
canvas,
new ZLAndroidPaintContext.Geometry(
getWidth(),
getHeight(),
getWidth(),
getMainAreaHeight(),
0,
0
),
view.isScrollbarShown() ? getVerticalScrollbarWidth() : 0
);
view.preparePage(context, ZLView.PageIndex.next);
}
});
}
});
}
public interface ZLViewEnums {
public enum PageIndex {
previous, current, next;
//忽略部分代码...
}
//忽略部分代码...
}
private void drawFooter(Canvas canvas, AnimationProvider animator) {
final ZLView view = ZLApplication.Instance().getCurrentView();
final ZLView.FooterArea footer = view.getFooterArea();
//忽略部分代码...
if (myFooterBitmap == null) {
myFooterBitmap = Bitmap.createBitmap(getWidth(), footer.getHeight(), Bitmap.Config.RGB_565);
}
final ZLAndroidPaintContext context = new ZLAndroidPaintContext(
mySystemInfo,
new Canvas(myFooterBitmap),
new ZLAndroidPaintContext.Geometry(
getWidth(),
getHeight(),
getWidth(),
footer.getHeight(),
0,
getMainAreaHeight()
),
view.isScrollbarShown() ? getVerticalScrollbarWidth() : 0
);
footer.paint(context);
final int voffset = getHeight() - footer.getHeight();
if (animator != null) {
animator.drawFooterBitmap(canvas, myFooterBitmap, voffset);
} else {
//传入的animator是null
canvas.drawBitmap(myFooterBitmap, 0, voffset, myPaint);
}
}
复制代码
这里可以看出干了三件事:
- 在(0,0)绘制一个bitmap,该bitmap是从BitmapManagerImpl中根据ZLView.PageIndex.current获取的
- 创建一个宽度为控件宽度,高度为footer.getHeight()的bitmap,随后调用当前类型footer的paint方法,在bitmap上绘制出要显示的内容。随后在(0,getHeight() - footer.getHeight())绘制该bitmap。
- 通过Executors去执行一个Runnable,其中传递参数ZLView.PageIndex为next
前两部比较比较清晰,是绘制了两个bitmap,那这两个biamap分别是什么呢?
debug看一下,第一个bitmap:
第二个bitmap:
再来看一下整个页面的显示效果:
额,手机截图不是很全,但是已经能够看出,最终结果是连个bitmap拼接后铺满了整个控件。而且从对上面整个过程的分析来看:FBReader绘制的时候,针对某一页page,都会去获取该页page对应的bitmap,然后再绘制在cavas上。
三、滑动翻页时的绘制
在翻页动画执行中,界面的显示是这样的(侧滑翻页):
在上面的分析过程中,已经知道如果当前翻页动画正在执行,那么onDraw会调用onDrawInScrolling来绘制页面内容:
private void onDrawInScrolling(Canvas canvas) {
//忽略部分代码...
final AnimationProvider animator = getAnimationProvider();//获取当前动画方式
//忽略部分代码...
animator.draw(canvas);//绘制页面内容
//忽略部分代码...
}
复制代码
这里我们就拿侧滑翻页动画来分析:
AnimationProvider.class
public final void draw(Canvas canvas) {
//忽略部分代码...
drawInternal(canvas);
//忽略部分代码...
}
protected void drawBitmapFrom(Canvas canvas, int x, int y, Paint paint) {
myBitmapManager.drawBitmap(canvas, x, y, ZLViewEnums.PageIndex.current, paint);
}
protected void drawBitmapTo(Canvas canvas, int x, int y, Paint paint) {
myBitmapManager.drawBitmap(canvas, x, y, getPageToScrollTo(), paint);
}
public final ZLViewEnums.PageIndex getPageToScrollTo() {
//根据滑动时的角标,获取下方显示的是上一页还是下一页
return getPageToScrollTo(myEndX, myEndY);
}
SimpleAnimationProvider.class extends AnimationProvider
@Override
public ZLViewEnums.PageIndex getPageToScrollTo(int x, int y) {
if (myDirection == null) {
return ZLViewEnums.PageIndex.current;
}
//myDirection表示如何滑动是正向,即能滑到下一页的滑动方向
switch (myDirection) {
case rightToLeft:
return myStartX < x ? ZLViewEnums.PageIndex.previous : ZLViewEnums.PageIndex.next;
case leftToRight:
return myStartX < x ? ZLViewEnums.PageIndex.next : ZLViewEnums.PageIndex.previous;
case up:
return myStartY < y ? ZLViewEnums.PageIndex.previous : ZLViewEnums.PageIndex.next;
case down:
return myStartY < y ? ZLViewEnums.PageIndex.next : ZLViewEnums.PageIndex.previous;
}
return ZLViewEnums.PageIndex.current;
}
SlideAnimationProvider.class extends SimpleAnimationProvider//侧滑翻页
@Override
protected void drawInternal(Canvas canvas) {
if (myDirection.IsHorizontal) {//水平方向翻页
final int dX = myEndX - myStartX;
setDarkFilter(dX, myWidth);//下面一页的半透明蒙层
drawBitmapTo(canvas, 0, 0, myDarkPaint);//绘制下面一页
drawBitmapFrom(canvas, dX, 0, myPaint);//绘制正在滑动的一页
drawShadowVertical(canvas, 0, myHeight, dX);//绘制分界线处的阴影
} else {//竖直翻页
final int dY = myEndY - myStartY;
setDarkFilter(dY, myHeight);
drawBitmapTo(canvas, 0, 0, myDarkPaint);
drawBitmapFrom(canvas, 0, dY, myPaint);
drawShadowHorizontal(canvas, 0, myWidth, dY);
}
}
复制代码
这个地方,其实也比较简单,原理就是根据滑动的方向和当前设置的翻页方式(水平翻页或竖直翻页),来获取底下的bitmap是上一页内容还是下一页内容。而当前跟随手指滑动发生位置变化的bitmap,就是currentPage对应的bitmap。而且是在绘制的时候是根据横向的滑动偏移dx,来确定canvas的绘制bitmap时的left,这样随着手指的移动,页面也就“动”了起来。
当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。