内容简介:先来回顾一下上一节最后说到的点,新角色FBReaderApp调用了openBookInternal方法:上一篇,我们已经分析过,在BookModel.createModel生成BookModel时,针对于epub格式的文件来说,最终会调用NativeFormatPlugin的readModelNative:这里有两个参数BookModel和cacheDir,我们先来看看BookModel是怎么生成的:
先来回顾一下上一节最后说到的点,新角色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,这样随着手指的移动,页面也就“动”了起来。
当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 开源电子书项目FBReader初探(一)
- 开源电子书项目FBReader初探(二)
- 开源电子书项目FBReader初探(三)
- 开源电子书项目FBReader初探(四)
- 开源电子书项目FBReader初探(六)
- 初探 Google 开源的 Python 命令行库
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。