开源电子书项目FBReader初探(五)

栏目: IOS · Android · 发布时间: 5年前

内容简介:先来回顾一下上一节最后说到的点,新角色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看一下:

开源电子书项目FBReader初探(五)

传入了一个路径给native,取名cache,看来navtive在解析电子书时会生成缓存?暂时把这个疑问放一边,去看一下BookModel这个类:

开源电子书项目FBReader初探(五)

有好多方法都是灰色的,证明在 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被调用时具体参数传递情况:

开源电子书项目FBReader初探(五)

很明显了,这个文件路径跟我们之前传递进去的路径是一个路径,文件扩展名是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看一下:

开源电子书项目FBReader初探(五)

地址还是我们传入的地址,但是这里文件类型变成了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生成的缓存文件:

开源电子书项目FBReader初探(五)

果然!这里有.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,看一下它是什么,在哪定义的:

开源电子书项目FBReader初探(五)

原来是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:

开源电子书项目FBReader初探(五)

第二个bitmap:

开源电子书项目FBReader初探(五)

再来看一下整个页面的显示效果:

开源电子书项目FBReader初探(五)
开源电子书项目FBReader初探(五)

额,手机截图不是很全,但是已经能够看出,最终结果是连个bitmap拼接后铺满了整个控件。而且从对上面整个过程的分析来看: FBReader绘制的时候,针对某一页page,都会去获取该页page对应的bitmap,然后再绘制在cavas上

三、滑动翻页时的绘制

在翻页动画执行中,界面的显示是这样的(侧滑翻页):

开源电子书项目FBReader初探(五)

在上面的分析过程中,已经知道如果当前翻页动画正在执行,那么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,这样随着手指的移动,页面也就“动”了起来。

当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大型网站系统与Java中间件开发实践

大型网站系统与Java中间件开发实践

曾宪杰 / 电子工业出版社 / 2014-4-24 / 65.00

本书围绕大型网站和支撑大型网站架构的 Java 中间件的实践展开介绍。从分布式系统的知识切入,让读者对分布式系统有基本的了解;然后介绍大型网站随着数据量、访问量增长而发生的架构变迁;接着讲述构建 Java 中间件的相关知识;之后的几章都是根据笔者的经验来介绍支撑大型网站架构的 Java 中间件系统的设计和实践。希望读者通过本书可以了解大型网站架构变迁过程中的较为通用的问题和解法,并了解构建支撑大型......一起来看看 《大型网站系统与Java中间件开发实践》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码