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解析BookModelModel = BookModel.createModel(book, plugin);//保存bookCollection.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
分别看一下,这几步都是做了些什么工作:
-
在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 cachemyModel = model;ExtensionManager = extManager;}@Overrideprotected 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) {//计数器清0myCounter = 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 databreak;case ZLTextParagraph.Entry.RESET_BIDI:// No databreak;case ZLTextParagraph.Entry.AUDIO:// No databreak;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归位于startcursorint 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();//获取所构建的段落对应的cursorfinal int wordIndex = result.getElementIndex();//开始的indexapplyStyleChanges(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到控件上。
当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。