内容简介:经过上一篇的分析,我们已经知道,FBRreader在绘制时是获取每一页对应的bitmap,然后再进行绘制的。同时,在绘制完当前页之后,会通过Executors.newSingleThreadExecutor来准备下一页的bitmap。上一篇提到了一个重要的角色——再次回到FBReaderApp这个类的openBookInternal,继续探索数据解析之后,内容的“瀑布”是怎么被开启的:
经过上一篇的分析,我们已经知道,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
分别看一下,这几步都是做了些什么工作:
-
在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到控件上。
当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 开源电子书项目FBReader初探(一)
- 开源电子书项目FBReader初探(二)
- 开源电子书项目FBReader初探(三)
- 开源电子书项目FBReader初探(四)
- 开源电子书项目FBReader初探(五)
- 初探 Google 开源的 Python 命令行库
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。