開源電子書專案FBReader初探(六)

CrazyCoder發表於2018-12-11

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:

開源電子書專案FBReader初探(六)

paint方法執行結束後bitmap:

開源電子書專案FBReader初探(六)

至此,當前page對應的bitmap就準備完成。通過bitmapmanager傳遞給ZLAndroidWidget,最終繪製此bitmap到控制元件上。

當然,由於本人接觸此專案時間有限,而且書寫技術文章的經驗實在欠缺,過程中難免會有存在錯誤或描述不清或語言累贅等等一些問題,還望大家能夠諒解,同時也希望大家繼續給予指正。最後,感謝大家對我的支援,讓我有了強大的動力堅持下去。

相關文章