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

2,574 阅读13分钟

FBReader是如何读取缓存文件内容,并生成每一页Bitmap内容的呢?

经过上一篇的分析,我们已经知道,FBRreader在绘制时是获取每一页对应的bitmap,然后再进行绘制的。同时,在绘制完当前页之后,会通过Executors.newSingleThreadExecutor来准备下一页的bitmap。

上一篇提到了一个重要的角色——ZLZLTextPlainModel。里面记录了native生成的缓存文件路径以及缓存文件个数。并且,其实例是在native解析BookModel时通过调用java方法创建并且set到BookModel中的。

一、数据注入——“瀑布”倾泻的开始

再次回到FBReaderApp这个类的openBookInternal,继续探索数据解析之后,内容的“瀑布”是怎么被开启的:

private synchronized void openBookInternal(final Book book, Bookmark bookmark, boolean force) {
        //忽略部分代码...
        try {
            //忽略部分代码...
            //native解析BookModel
            Model = BookModel.createModel(book, plugin);
            //保存book
            Collection.saveBook(book);
            ZLTextHyphenator.Instance().load(book.getLanguage());
            //数据注入
            BookTextView.setModel(Model.getTextModel());
            //忽略部分代码...
        } catch (BookReadingException e) {
            processException(e);
        }

        getViewWidget().reset();
        getViewWidget().repaint();

        //忽略部分代码...
}

这里有一个核心的方法,会将数据注入到view中:

BookTextView.setModel(Model.getTextModel());

这里的BookTextView为FBView的实例,追溯其setModel方法,最终在ZLTextView中:

public synchronized void setModel(ZLTextModel model) {
    myCursorManager = model != null ? new CursorManager(model, getExtensionManager()) : null;

    //忽略部分代码...
    
    myModel = model;
    myCurrentPage.reset();
    myPreviousPage.reset();
    myNextPage.reset();
    if (myModel != null) {
        final int paragraphsNumber = myModel.getParagraphsNumber();
        if (paragraphsNumber > 0) {
            myCurrentPage.moveStartCursor(myCursorManager.get(0));
        }
    }
    Application.getViewWidget().reset();
}

这里有这么几件事需要注意一下:

  • 判空model,生成CursorManager
  • 重置上一页、当前页、下一页
  • 判空myModel,通过CursorManager获取第一自然段的cursor
  • 将当前currentpage内容起始位置指向第一自然段的cursor
  • 重置Application.getViewWidget

分别看一下,这几步都是做了些什么工作:

  1. 在model不为空的情况下会创建CusorManger,那么这个CusorManger是什么呢?

     final class CursorManager extends LruCache<Integer,ZLTextParagraphCursor> {
         private final ZLTextModel myModel;
         final ExtensionElementManager ExtensionManager;
    
         CursorManager(ZLTextModel model, ExtensionElementManager extManager) {
     	super(200); // max 200 cursors in the cache
     	myModel = model;
     	ExtensionManager = extManager;
         }
    
         @Override
         protected ZLTextParagraphCursor create(Integer index) {
             return new ZLTextParagraphCursor(this, myModel, index);
         }
     }
    

    原来CusorManger是继承自LruCache<Integer,ZLTextParagraphCursor>,而且其最大缓存200个cursor,并且重写create方法,在调用get(integer)时,如果获取不到则会通过create创建integer对应的ZLTextParagraphCurosr对象。

    再来看一下ZLTextParagraphCurosr,该类是第index自然段的cursor:

     public final class ZLTextParagraphCursor {
         //忽略部分代码...
         ZLTextParagraphCursor(CursorManager cManager, ZLTextModel model, int index) {
             CursorManager = cManager;
             Model = model;
             //段落角标
             Index = Math.min(index, model.getParagraphsNumber() - 1);
             fill();
         }
         //忽略部分代码...
     }
    

2.重置上一页、当前页、下一页(ZLTextPage)

final class ZLTextPage {
    final ZLTextWordCursor StartCursor = new ZLTextWordCursor();
    final ZLTextWordCursor EndCursor = new ZLTextWordCursor();
    final ArrayList<ZLTextLineInfo> LineInfos = new ArrayList<ZLTextLineInfo>();
    int PaintState = PaintStateEnum.NOTHING_TO_PAINT;
    
    void reset() {
        StartCursor.reset();
        EndCursor.reset();
        LineInfos.clear();
        PaintState = PaintStateEnum.NOTHING_TO_PAINT;
    }
}

看起来好像每一页的内容范围,是由起始的starCurosr和终止的endCursor定位的?来看看ZLTextWordCursor:

public final class ZLTextWordCursor extends ZLTextPosition {
    private ZLTextParagraphCursor myParagraphCursor;
    private int myElementIndex;
    private int myCharIndex;
    
    public void reset() {
	myParagraphCursor = null;
	myElementIndex = 0;
	myCharIndex = 0;
    }
    
}

3.判空model,不为空时获取cursormanager.get(0),我们知道在初始创建cursormanager时,内部是没有缓存的内容的,这时会通过create创建ZLTextParagraphCursor对象。

4.将当前页的起始curosr移动至上一步获取的curosr处,并将endcuror重置:

ZLTextPage.class
final ArrayList<ZLTextLineInfo> LineInfos = new ArrayList<ZLTextLineInfo>();
void moveStartCursor(ZLTextParagraphCursor cursor) {
    StartCursor.setCursor(cursor);
    EndCursor.reset();
    LineInfos.clear();
    PaintState = PaintStateEnum.START_IS_KNOWN;
}

ZLTextWordCursor.class
public void setCursor(ZLTextParagraphCursor paragraphCursor) {
    myParagraphCursor = paragraphCursor;
    myElementIndex = 0;
    myCharIndex = 0;
}

5.重置Application.getViewWidget的重置,最终在bitmapmanager中:

void reset() {
    for (int i = 0; i < SIZE; ++i) {
        myIndexes[i] = null;//置空缓存的bitmap
    }
}

二、ZLTextParagraphCursor开启数据仓库的“大门”

在ZLTextParagraphCursor初始化时,调用fill方法:

ZLTextParagraphCursor(CursorManager cManager, ZLTextModel model, int index) {
    CursorManager = cManager;
    Model = model;
    Index = Math.min(index, model.getParagraphsNumber() - 1);
    fill();
}

void fill() {
    ZLTextParagraph	paragraph = Model.getParagraph(Index);
    switch (paragraph.getKind()) {
        case ZLTextParagraph.Kind.TEXT_PARAGRAPH:
            new Processor(paragraph, CursorManager.ExtensionManager, new LineBreaker(Model.getLanguage()), Model.getMarks(), Index, myElements).fill();
            break;
        //忽略部分代码...
    }
}

发现会通过model获取index自然段对应的paragraph,我们知道model为ZLTextPlainModel的实例:

public final ZLTextParagraph getParagraph(int index) {
    //获取index自然段的kind,数组myParagraphKinds数据由native解析得到
    final byte kind = myParagraphKinds[index];
    return (kind == ZLTextParagraph.Kind.TEXT_PARAGRAPH) ?
        new ZLTextParagraphImpl(this, index) :
        new ZLTextSpecialParagraphImpl(kind, this, index);
}

一般的情况下,自然段均为TEXT_PARAGRAPH,相应的就会生成ZLTextParagraphImpl:

class ZLTextParagraphImpl implements ZLTextParagraph {
    private final ZLTextPlainModel myModel;
    private final int myIndex;

    ZLTextParagraphImpl(ZLTextPlainModel model, int index) {
	myModel = model;
	myIndex = index;
    }

    public EntryIterator iterator() {
	return myModel.new EntryIteratorImpl(myIndex);
    }

    public byte getKind() {
	return Kind.TEXT_PARAGRAPH;
    }
}

这里有一个地方需要注意,那就是iterator()方法返回的迭代器对象EntryIteratorImpl:

tips: EntryIteratorImpl为ZLTextPlainModel的非静态内部类

EntryIteratorImpl(int index) {
   reset(index);
}

void reset(int index) {
    //计数器清0
    myCounter = 0;
    //获取native读取后,index段落内容长度
    myLength = myParagraphLengths[index];
    //获取native读取后,index段落内容在哪个ncache文件中
    myDataIndex = myStartEntryIndices[index];
    //获取native读取后,index段落内容起始位置在ncache内容中的偏移
    myDataOffset = myStartEntryOffsets[index];
}

接下来,由于段落类型为TEXT_PARAGRAPH,那么就会执行new Processor(...).fill():

void fill() {
    //忽略部分代码...
    final ArrayList<ZLTextElement> elements = myElements;
    for (ZLTextParagraph.EntryIterator it = myParagraph.iterator(); it.next(); ) {
        switch (it.getType()) {
            case ZLTextParagraph.Entry.TEXT:
                processTextEntry(it.getTextData(), it.getTextOffset(), it.getTextLength(), hyperlink);
                break;
            case ZLTextParagraph.Entry.CONTROL:
                //忽略部分代码...
                break;
            case ZLTextParagraph.Entry.HYPERLINK_CONTROL:
                //忽略部分代码...
                break;
            case ZLTextParagraph.Entry.IMAGE:
                final ZLImageEntry imageEntry = it.getImageEntry();
                final ZLImage image = imageEntry.getImage();
                if (image != null) {
                    ZLImageData data = ZLImageManager.Instance().getImageData(image);
                    if (data != null) {
                        if (hyperlink != null) {
                            hyperlink.addElementIndex(elements.size());
                        }
                        elements.add(new ZLTextImageElement(imageEntry.Id, data, image.getURI(), imageEntry.IsCover));
                    }
                }
                break;
            case ZLTextParagraph.Entry.AUDIO:
                break;
            case ZLTextParagraph.Entry.VIDEO:
                break;
            case ZLTextParagraph.Entry.EXTENSION:
                //忽略部分代码...
                break;
            case ZLTextParagraph.Entry.STYLE_CSS:
            case ZLTextParagraph.Entry.STYLE_OTHER:
                elements.add(new ZLTextStyleElement(it.getStyleEntry()));
                break;
            case ZLTextParagraph.Entry.STYLE_CLOSE:
                elements.add(ZLTextElement.StyleClose);
                break;
            case ZLTextParagraph.Entry.FIXED_HSPACE:
                elements.add(ZLTextFixedHSpaceElement.getElement(it.getFixedHSpaceLength()));
                break;
        }
    }
}

这里会进入一个for循环,循环的条件是it.next(),而it是myParagraph.iterator(),这个上一步我们已经分析过,针对kind为TEXT_PARAGRAPH的自然段,iterator返回的对象为EntryIteratorImpl,那么就看一下EntryIteratorImpl的next方法:

public boolean next() {
    if (myCounter >= myLength) {
        return false;
    }

    int dataOffset = myDataOffset;//该段落起始游标
    char[] data = myStorage.block(myDataIndex);
    if (data == null) {
	    return false;
    }
    if (dataOffset >= data.length) {
        data = myStorage.block(++myDataIndex);
        if (data == null) {
            return false;
        }
        dataOffset = 0;
    }
    short first = (short)data[dataOffset];
    byte type = (byte)first;
    if (type == 0) {
        data = myStorage.block(++myDataIndex);
        if (data == null) {
            return false;
        }
        dataOffset = 0;
        first = (short)data[0];
        type = (byte)first;
        }
    myType = type;
    ++dataOffset;
    switch (type) {
        case ZLTextParagraph.Entry.TEXT:
        {
            int textLength = (int)data[dataOffset++];
            textLength += (((int)data[dataOffset++]) << 16);
            textLength = Math.min(textLength, data.length - dataOffset);
            myTextLength = textLength;
            myTextData = data;
            myTextOffset = dataOffset;
            dataOffset += textLength;
            break;
        }
        case ZLTextParagraph.Entry.CONTROL:
        {
            //忽略部分代码...
            break;
        }
        case ZLTextParagraph.Entry.HYPERLINK_CONTROL:
        {
            //忽略部分代码...
            break;
        }
        case ZLTextParagraph.Entry.IMAGE:
        {
            final short vOffset = (short)data[dataOffset++];
            final short len = (short)data[dataOffset++];
            final String id = new String(data, dataOffset, len);
            dataOffset += len;
            final boolean isCover = data[dataOffset++] != 0;
            myImageEntry = new ZLImageEntry(myImageMap, id, vOffset, isCover);
            break;
        }
        case ZLTextParagraph.Entry.FIXED_HSPACE:
            //忽略部分代码...
            break;
        case ZLTextParagraph.Entry.STYLE_CSS:
        case ZLTextParagraph.Entry.STYLE_OTHER:
        {
            //忽略部分代码...
        }
        case ZLTextParagraph.Entry.STYLE_CLOSE:
        // No data
            break;
        case ZLTextParagraph.Entry.RESET_BIDI:
            // No data
            break;
        case ZLTextParagraph.Entry.AUDIO:
            // No data
            break;
        case ZLTextParagraph.Entry.VIDEO:
        {
            //忽略部分代码...
            break;
        }
        case ZLTextParagraph.Entry.EXTENSION:
        {
            //忽略部分代码...
            break;
        }
    }
    ++myCounter;
    myDataOffset = dataOffset;
    return true;
}

在next方法中,出现了之前分析到的一个角色CachedCharStorage,首先会调用其block方法:

protected final ArrayList<WeakReference<char[]>> myArray =
	new ArrayList<WeakReference<char[]>>();
	
public char[] block(int index) {
    if (index < 0 || index >= myArray.size()) {
        return null;
    }
    char[] block = myArray.get(index).get();
    if (block == null) {
        try {
            File file = new File(fileName(index));
            int size = (int)file.length();
            if (size < 0) {
                throw new CachedCharStorageException(exceptionMessage(index, "size = " + size));
            }
            block = new char[size / 2];
            InputStreamReader reader =
                new InputStreamReader(
                    new FileInputStream(file),
                    "UTF-16LE"
                );
            final int rd = reader.read(block);
            if (rd != block.length) {
                throw new CachedCharStorageException(exceptionMessage(index, "; " + rd + " != " + block.length));
            }
            reader.close();
        } catch (IOException e) {
            throw new CachedCharStorageException(exceptionMessage(index, null), e);
        }
        myArray.set(index, new WeakReference<char[]>(block));
    }
    return block;
}

在调用block方法时,传入的参数为myDataIndex,该参数指明了当前自然段的内容在哪个ncahce文件中。不难分析出,next方法主要的作用:

  • 读取要获取的自然段所在ncache,如果CachedCharStorage中已缓存则取缓存,否则直接读取对应的ncache文件
  • 必要时读取下一个ncache文件(当前段落内容起始在x.ncache中,但终止在x+1.ncahce中)
  • 根据native读取的段落内容长度,每次调用next读取一个内容元素,并将读取到的元素类型(可能是TEXT、IMAGE等格式)、数据内容、offset、长度等记录下来

这里,我们再次回到for循环。通过next方法,我们已经知道,该方法会读取一个元素,并将读取到的元素类型等信息保存下来,查看for循环内部代码发现,后续会根据读取到的元素类型,进行数据的原始组装,并最终保存到ZLTextParagraphCursor的ArrayList集合中。即通过此fill方法最终将index自然段的每一个元素读取出来,并存入了集合中。

三、通过数据仓库“大门”,拉取所需内容数据,绘制页面对应bitmap

在初始化ZLTextParagraphCursor时,我们已经知道其通过fill方法,已经将内容解析出来。这时,我们再回看一下setModel方法:

public synchronized void setModel(ZLTextModel model) {
    //忽略部分代码...
    if (myModel != null) {
	final int paragraphsNumber = myModel.getParagraphsNumber();
	if (paragraphsNumber > 0) {
	    myCurrentPage.moveStartCursor(myCursorManager.get(0));
	}
    }
    //忽略部分代码...
}

会将当前页面的startCursor移动到第一自然段,并将当前页面的PaintState设置为START_IS_KNOWN。这个时候页面已经准备就绪,等待“发令枪”响了!那么“发令枪”,是在什么时候打响的呢?这就又要回顾一下之前的一个老朋友,FBReader界面唯一的控件——ZLAndroidWidget。它的onDraw方法我们已经分析过,在静止状态时,会调用onDrawStatic:

ZLAndroidWidget.class
private void onDrawStatic(final Canvas canvas) {
    canvas.drawBitmap(myBitmapManager.getBitmap(ZLView.PageIndex.current), 0, 0, myPaint);
    //忽略部分代码...
}

BitmapManagerImpl.class
public Bitmap getBitmap(ZLView.PageIndex index) {
    //忽略部分代码...
    myWidget.drawOnBitmap(myBitmaps[iIndex], index);
    return myBitmaps[iIndex];
}

ZLAndroidWidget.class
void drawOnBitmap(Bitmap bitmap, ZLView.PageIndex index) {
    final ZLView view = ZLApplication.Instance().getCurrentView();
    if (view == null) {
        return;
    }

    final ZLAndroidPaintContext context = new ZLAndroidPaintContext(
        mySystemInfo,
        new Canvas(bitmap),
        new ZLAndroidPaintContext.Geometry(
            getWidth(),
            getHeight(),
            getWidth(),
            getMainAreaHeight(),
            0,
            0
        ),
        view.isScrollbarShown() ? getVerticalScrollbarWidth() : 0
    );
    view.paint(context, index);
}

ZLApplication.Instance().getCurrentView()返回的对象即为setModel时的BookTextView,那么就会调用其paint方法:

public synchronized void paint(ZLPaintContext context, PageIndex pageIndex) {
    setContext(context);
    final ZLFile wallpaper = getWallpaperFile();
    if (wallpaper != null) {
        context.clear(wallpaper, getFillMode());
    } else {
        context.clear(getBackgroundColor());
    }

    if (myModel == null || myModel.getParagraphsNumber() == 0) {
        return;
    }

    ZLTextPage page;
    switch (pageIndex) {
        default:
        case current:
            page = myCurrentPage;
            break;
        case previous:
            page = myPreviousPage;
            if (myPreviousPage.PaintState == PaintStateEnum.NOTHING_TO_PAINT) {
                preparePaintInfo(myCurrentPage);
                myPreviousPage.EndCursor.setCursor(myCurrentPage.StartCursor);
                myPreviousPage.PaintState = PaintStateEnum.END_IS_KNOWN;
            }
            break;
        case next:
            page = myNextPage;
            if (myNextPage.PaintState == PaintStateEnum.NOTHING_TO_PAINT) {
                preparePaintInfo(myCurrentPage);
                myNextPage.StartCursor.setCursor(myCurrentPage.EndCursor);
                myNextPage.PaintState = PaintStateEnum.START_IS_KNOWN;
            }
    }

    page.TextElementMap.clear();

    preparePaintInfo(page);

    if (page.StartCursor.isNull() || page.EndCursor.isNull()) {
        return;
    }

    final ArrayList<ZLTextLineInfo> lineInfos = page.LineInfos;
    final int[] labels = new int[lineInfos.size() + 1];
    int x = getLeftMargin();
    int y = getTopMargin();
    int index = 0;
    int columnIndex = 0;
    ZLTextLineInfo previousInfo = null;
    for (ZLTextLineInfo info : lineInfos) {
        info.adjust(previousInfo);
        prepareTextLine(page, info, x, y, columnIndex);
        y += info.Height + info.Descent + info.VSpaceAfter;
        labels[++index] = page.TextElementMap.size();
        if (index == page.Column0Height) {
            y = getTopMargin();
            x += page.getTextWidth() + getSpaceBetweenColumns();
            columnIndex = 1;
        }
        previousInfo = info;
    }

    final List<ZLTextHighlighting> hilites = findHilites(page);

    x = getLeftMargin();
    y = getTopMargin();
    index = 0;
    for (ZLTextLineInfo info : lineInfos) {
        drawTextLine(page, hilites, info, labels[index], labels[index + 1]);
        y += info.Height + info.Descent + info.VSpaceAfter;
        ++index;
        if (index == page.Column0Height) {
            y = getTopMargin();
            x += page.getTextWidth() + getSpaceBetweenColumns();
        }
    }
    //忽略部分代码...
}

1.会获取当前设置的墙纸,如果能获取到墙纸,那么会再去获取墙纸的绘制方式,根据不同的方式,最终将墙纸绘制到bitmap上。

2.根据页面Index,获取对应的page对象。

3.获取到当前要绘制的page对象后,通过preparePaintInfo方法,根据当前page的PaintState,构建页面基础元素信息,这里会给page设置size(可绘制区域宽高以及是否是双列绘制等)

private synchronized void preparePaintInfo(ZLTextPage page) {
    page.setSize(getTextColumnWidth(), getTextAreaHeight(), twoColumnView(), page == myPreviousPage);
    //忽略部分代码...
    final int oldState = page.PaintState;

    final HashMap<ZLTextLineInfo,ZLTextLineInfo> cache = myLineInfoCache;
    for (ZLTextLineInfo info : page.LineInfos) {
        cache.put(info, info);
    }

    switch (page.PaintState) {
        default:
            break;
        case PaintStateEnum.TO_SCROLL_FORWARD:
            //忽略部分代码...
            break;
        case PaintStateEnum.TO_SCROLL_BACKWARD:
            //忽略部分代码...
            break;
        case PaintStateEnum.START_IS_KNOWN:
            if (!page.StartCursor.isNull()) {
                buildInfos(page, page.StartCursor, page.EndCursor);
            }
            break;
        case PaintStateEnum.END_IS_KNOWN:
            //忽略部分代码...
            break;
    }
    page.PaintState = PaintStateEnum.READY;
    // TODO: cache?
    myLineInfoCache.clear();

    if (page == myCurrentPage) {
        if (oldState != PaintStateEnum.START_IS_KNOWN) {
            myPreviousPage.reset();
        }
        if (oldState != PaintStateEnum.END_IS_KNOWN) {
            myNextPage.reset();
        }
    }
}

4.通过之前的分析,当前页面的PaintState在moveStartCursor时被设置为了START_IS_KNOWN,那么就会调用buildInfos方法,去构建页面原始数据信息:

private void buildInfos(ZLTextPage page, ZLTextWordCursor start, ZLTextWordCursor result) {
    result.setCursor(start);//将endcursor归位于startcursor
    int textAreaHeight = page.getTextHeight();//获取当前页面可绘制内容区域高度
    page.LineInfos.clear();//清空之前构建信息
    page.Column0Height = 0;//记录第一列已构建高度
    boolean nextParagraph;//是否是下一自然段
    ZLTextLineInfo info = null;//构建的行内容信息
    do {
        final ZLTextLineInfo previousInfo = info;
        resetTextStyle();
        final ZLTextParagraphCursor paragraphCursor result.getParagraphCursor();//获取所构建的段落对应的cursor
        final int wordIndex = result.getElementIndex();//开始的index
        applyStyleChanges(paragraphCursor, 0, wordIndex);
        info = new ZLTextLineInfo(paragraphCursor, wordIndex, result.getCharIndex(), getTextStyle());//构建一个行信息
        final int endIndex = info.ParagraphCursorLength;//结束index(段落内容长度)
        while (info.EndElementIndex != endIndex) {
            info = processTextLine(page, paragraphCursor, info.EndElementIndex, info.EndCharIndex, endIndex, previousInfo);
            textAreaHeight -= info.Height + info.Descent;
            if (textAreaHeight < 0 && page.LineInfos.size() > page.Column0Height) {
                if (page.Column0Height == 0 && page.twoColumnView()) {
                    textAreaHeight = page.getTextHeight();
                    textAreaHeight -= info.Height + info.Descent;
                    page.Column0Height = page.LineInfos.size();
                } else {
                    break;
                }
            }
            textAreaHeight -= info.VSpaceAfter;
            result.moveTo(info.EndElementIndex, info.EndCharIndex);
            page.LineInfos.add(info);
            if (textAreaHeight < 0) {
                if (page.Column0Height == 0 && page.twoColumnView()) {
                    textAreaHeight = page.getTextHeight();
                    page.Column0Height = page.LineInfos.size();
                } else {
                    break;
                }
            }
        }
        //如果当前已经读取到了该段落最后位置,则获取下一段落
        nextParagraph = result.isEndOfParagraph() && result.nextParagraph();
        if (nextParagraph && result.getParagraphCursor().isEndOfSection()) {
            if (page.Column0Height == 0 && page.twoColumnView() && !page.LineInfos.isEmpty()) {
                textAreaHeight = page.getTextHeight();
                page.Column0Height = page.LineInfos.size();
            }
        }
    } while (nextParagraph && textAreaHeight >= 0 &&
             (!result.getParagraphCursor().isEndOfSection() ||
                page.LineInfos.size() == page.Column0Height)
            );
    resetTextStyle();
}

private ZLTextLineInfo processTextLine(
    ZLTextPage page,
    ZLTextParagraphCursor paragraphCursor,
    final int startIndex,
    final int startCharIndex,
    final int endIndex,
    ZLTextLineInfo previousInfo
) {
    final ZLTextLineInfo info = processTextLineInternal(
        page, paragraphCursor, startIndex, startCharIndex, endIndex, previousInfo
    );
    if (info.EndElementIndex == startIndex && info.EndCharIndex == startCharIndex) {
        info.EndElementIndex = paragraphCursor.getParagraphLength();
        info.EndCharIndex = 0;
        // TODO: add error element
    }
    return info;
}

private ZLTextLineInfo processTextLineInternal(
	ZLTextPage page,
	ZLTextParagraphCursor paragraphCursor,
	final int startIndex,
	final int startCharIndex,
	final int endIndex,
	ZLTextLineInfo previousInfo
){
    //忽略部分代码...
}

已第一次阅读时的构建场景为例,通过buildInfos方法,针对要构建内容的page,会做如下几件事:

  • page的startCusor在之前被移动到了第一自然段,并且第一自然段在创建时已读取出来。在此方法中,会遍历已读取出的自然段内容元素
  • 遍历元素过程中,会根据可绘制区域宽度,一行一行的构建出行元素信息,且每一行的高度为行内元素中高度最高元素的高度
  • 生产出的每一行元素,再根据可绘制区域高度,判断该行是否能够添加到页面中。如果能,则加入并继续构建下一行;如果不能则退出构建,当前页面元素构建完毕
  • 如果针对于第一自然段,遍历完每一个元素,切构建完每一行的行元素后,当前仍有可用绘制高度,则获取下一自然段,继续重复上述步骤,构建行信息,直至构建结束

到此,已经根据实际的可用空间,构建出了当前page的内容数据,并且是一行一行的内容数据。每一行中,包含着之前读取出的数据元素。

5.包装元素,将元素转变为可以被cavas绘制的元素“区域”

经过上面的页面数据构建,已经将page当前情况下的数据内容一行行的构建出来了。但是,目前构建出来的数据,还是只是数据,而我们最终的目的是生成page对应的bitmap。那么就需要对每一行的每一个元素进行位置描述,转变为页面上一个一个的具有真实位置和数据信息的内容。而这一步的转变,是通过for遍历每一行完成的:

x、y为元素绘制坐标
for (ZLTextLineInfo info : lineInfos) {
    info.adjust(previousInfo);
    //将每一行中的每一个元素包装为元素“区域”(带有元素数据和绘制坐标)
    prepareTextLine(page, info, x, y, columnIndex);
    y += info.Height + info.Descent + info.VSpaceAfter;
    labels[++index] = page.TextElementMap.size();
    if (index == page.Column0Height) {
        y = getTopMargin();
        x += page.getTextWidth() + getSpaceBetweenColumns();
        columnIndex = 1;
    }
    previousInfo = info;
}

6.绘制每一行的每一行元素“区域”

元素“区域”包装完成,可以进行绘制了:

for (ZLTextLineInfo info : lineInfos) {
    drawTextLine(page, hilites, info, labels[index], labels[index + 1]);
    y += info.Height + info.Descent + info.VSpaceAfter;
    ++index;
    if (index == page.Column0Height) {
        y = getTopMargin();
        x += page.getTextWidth() + getSpaceBetweenColumns();
    }
}

private void drawTextLine(ZLTextPage page, List<ZLTextHighlighting> hilites, ZLTextLineInfo info, int from, int to) {
    final ZLPaintContext context = getContext();
    final ZLTextParagraphCursor paragraph = info.ParagraphCursor;
    int index = from;
    final int endElementIndex = info.EndElementIndex;
    int charIndex = info.RealStartCharIndex;
    final List<ZLTextElementArea> pageAreas = page.TextElementMap.areas();
    if (to > pageAreas.size()) {
        return;
    }
    for (int wordIndex = info.RealStartElementIndex; wordIndex != endElementIndex && index < to; ++wordIndex, charIndex = 0) {
        final ZLTextElement element = paragraph.getElement(wordIndex);
        final ZLTextElementArea area = pageAreas.get(index);
        if (element == area.Element) {
            ++index;
            if (area.ChangeStyle) {
                setTextStyle(area.Style);
            }
            final int areaX = area.XStart;
            final int areaY = area.YEnd - getElementDescent(element) - getTextStyle().getVerticalAlign(metrics());
            if (element instanceof ZLTextWord) {
                final ZLTextPosition pos =
                    new ZLTextFixedPosition(info.ParagraphCursor.Index, wordIndex, 0);
                final ZLTextHighlighting hl = getWordHilite(pos, hilites);
                final ZLColor hlColor = hl != null ? hl.getForegroundColor() : null;
                drawWord(
                    areaX, areaY, (ZLTextWord)element, charIndex, -1, false,
                    hlColor != null ? hlColor : getTextColor(getTextStyle().Hyperlink)
                );
            } else if (element instanceof ZLTextImageElement) {
                final ZLTextImageElement imageElement = (ZLTextImageElement)element;
                context.drawImage(
                    areaX, areaY,
                    imageElement.ImageData,
                    getTextAreaSize(),
                    getScalingType(imageElement),
                    getAdjustingModeForImages()
                );
            } else if (element instanceof ZLTextVideoElement) {
                //忽略部分代码...
            } else if (element instanceof ExtensionElement) {
                //忽略部分代码...
            } else if (element == ZLTextElement.HSpace || element == ZLTextElement.NBSpace) {
                //忽略部分代码...
            }
        }
    }
    //忽略部分代码...
}

7.绘制执行者——ZLAndroidPaintContext

最终的绘制,是有此类对象来执行,查看其主要的两个方法:

public void drawString(int x, int y, char[] string, int offset, int length) {
    boolean containsSoftHyphen = false;
    for (int i = offset; i < offset + length; ++i) {
        if (string[i] == (char)0xAD) {
            containsSoftHyphen = true;
            break;
        }
    }
    if (!containsSoftHyphen) {
        myCanvas.drawText(string, offset, length, x, y, myTextPaint);
    } else {
        final char[] corrected = new char[length];
        int len = 0;
        for (int o = offset; o < offset + length; ++o) {
            final char chr = string[o];
            if (chr != (char)0xAD) {
                corrected[len++] = chr;
            }
        }
        myCanvas.drawText(corrected, 0, len, x, y, myTextPaint);
    }
}

public void drawImage(int x, int y, ZLImageData imageData, Size maxSize, ScalingType scaling, ColorAdjustingMode adjustingMode) {
    final Bitmap bitmap = ((ZLAndroidImageData)imageData).getBitmap(maxSize, scaling);
    if (bitmap != null && !bitmap.isRecycled()) {
        switch (adjustingMode) {
            case LIGHTEN_TO_BACKGROUND:
                myFillPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN));
                break;
            case DARKEN_TO_BACKGROUND:
                myFillPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));
                break;
            case NONE:
                break;
        }
        myCanvas.drawBitmap(bitmap, x, y - bitmap.getHeight(), myFillPaint);
        myFillPaint.setXfermode(null);
    }
}

8.paint方法前后bitmap内容对比

起初bitmap:

paint方法执行结束后bitmap:

至此,当前page对应的bitmap就准备完成。通过bitmapmanager传递给ZLAndroidWidget,最终绘制此bitmap到控件上。

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