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
分別看一下,這幾步都是做了些什麼工作:
-
在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到控制元件上。
當然,由於本人接觸此專案時間有限,而且書寫技術文章的經驗實在欠缺,過程中難免會有存在錯誤或描述不清或語言累贅等等一些問題,還望大家能夠諒解,同時也希望大家繼續給予指正。最後,感謝大家對我的支援,讓我有了強大的動力堅持下去。