開源電子書專案FBReader初探(六)
經過上一篇的分析,我們已經知道,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() { ZLTextParagraphparagraph = 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到控制元件上。
當然,由於本人接觸此專案時間有限,而且書寫技術文章的經驗實在欠缺,過程中難免會有存在錯誤或描述不清或語言累贅等等一些問題,還望大家能夠諒解,同時也希望大家繼續給予指正。最後,感謝大家對我的支援,讓我有了強大的動力堅持下去。