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

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

内容简介:先来回顾一下上一节最后说到的点,新角色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,这样随着手指的移动,页面也就“动”了起来。

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


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

查看所有标签

猜你喜欢:

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

React 进阶之路

React 进阶之路

徐超 / 清华大学出版社 / 2018-4 / 69.00元

《React进阶之路》详细介绍了React技术栈涉及的主要技术。本书分为基础篇、进阶篇和实战篇三部分。基础篇主要介绍React的基本用法,包括React 16的新特性;进阶篇深入讲解组件state、虚拟DOM、高阶组件等React中的重要概念,同时对初学者容易困惑的知识点做了介绍;实战篇介绍React Router、Redux和MobX 3个React技术栈的重要成员,并通过实战项目讲解这些技术如......一起来看看 《React 进阶之路》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具