How does FBReader read the contents of cached files and generate the contents of each Bitmap page?

As we know from the previous analysis, FBRreader draws a bitmap for each page and then draws it. At the same time, after drawing the current page, by Executors. NewSingleThreadExecutor bitmap to prepare for the next page.

The last article mentioned an important role – ZLZLTextPlainModel. It records the path and number of cached files generated by Native. Furthermore, the example is created and set into the BookModel by calling a Java method when parsing the BookModel native.

Data injection — the beginning of the cascade

Go back to openBookInternal of the FBReaderApp class and explore how the “waterfall” of content is turned on after data parsing:

Private synchronized void openBookInternal(Final Book Book, Bookmark Bookmark, Boolean force) {private synchronized void openBookInternal(final Book Book, Bookmark Bookmark, Boolean force) Try {// Ignore some code... BookModel = bookModel.createmodel (book, plugin); // Native BookModel = bookModel.createmodel (book, plugin); // saveBook collection.savebook (book); ZLTextHyphenator.Instance().load(book.getLanguage()); // Data injection bookTextView.setModel (model.gettextModel ()); // Ignore some code... } catch (BookReadingException e) { processException(e); } getViewWidget().reset(); getViewWidget().repaint(); // Ignore some code... }Copy the code

Here is a core method that injects data into the view:

BookTextView.setModel(Model.getTextModel());
Copy the code

Here BookTextView is an instance of FBView, tracing its setModel method to ZLTextView:

public synchronized void setModel(ZLTextModel model) { myCursorManager = model ! = null ? new CursorManager(model, getExtensionManager()) : null; // Ignore some code... 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(); }Copy the code

Here are a few things to note:

  • Void model to generate CursorManager
  • Reset previous page, current page, next page
  • Void myModel and use CursorManager to get the first paragraph’s cursor
  • Points the start position of the current CurrentPage content to the cursor in the first paragraph
  • Reset Application. GetViewWidget

Take a look at what each step does:

  1. What is a CusorManger that is created when the Model is not empty?

    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); }}Copy the code

    Originally CusorManger is inherited from LruCache < Integer, ZLTextParagraphCursor >, and its maximum cache 200 cursor, and rewrite the create method, get the call (Integer), If it does not, create creates integer’s corresponding ZLTextParagraphCurosr object.

    Look again at ZLTextParagraphCurosr, which is the cursor for the index paragraph:

    Public Final Class ZLTextParagraphCursor {// Ignore some code... ZLTextParagraphCursor(CursorManager cManager, ZLTextModel model, int index) { CursorManager = cManager; Model = model; / / paragraphs Angle standard Index = Math. Min (Index, the model getParagraphsNumber () - 1); fill(); } // Ignore some code... }Copy the code

2. Reset previous page, current page, next page (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; }}Copy the code

Does it look as if the content range of each page is located by starCurosr and endCursor? Let’s look at 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; }}Copy the code

3. Empty model, and get cursorManager.get (0) if it is not empty. We know that when cursorManager is initially created, there is no internal cached content, and then create an ZLTextParagraphCursor object.

4. Move the start curosr of the current page to the curosr obtained in the previous step and reset 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;
}
Copy the code

5. Reset Application. GetViewWidget reset, finally in bitmapmanager:

void reset() { for (int i = 0; i < SIZE; ++i) { myIndexes[i] = null; // Empty the cache bitmap}}Copy the code

Ii. ZLTextParagraphCursor Opens the Door to Data Warehouse

When ZLTextParagraphCursor is initialized, the fill method is called:

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; // Ignore some code... }}Copy the code

Paragraph corresponding to the index paragraph will be obtained through model. We know that Model is an instance of ZLTextPlainModel:

Public final ZLTextParagraph getParagraph(int index) {public final ZLTextParagraph getParagraph(int index) { Array myParagraphKinds Data is parsed by native and final Byte kind = myParagraphKinds[index]; return (kind == ZLTextParagraph.Kind.TEXT_PARAGRAPH) ? new ZLTextParagraphImpl(this, index) : new ZLTextSpecialParagraphImpl(kind, this, index); }Copy the code

In general, the paragraph is TEXT_PARAGRAPH, which generates 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; }}Copy the code

One thing to note here is that the iterator object returned by the iterator() method is EntryIteratorImpl:

EntryIteratorImpl(int index) {reset(index); } void reset(int index) {// myCounter = 0; // After getting native reads, myLength = myParagraphLengths[index]; MyDataIndex = myStartEntryIndices[index]; myDataIndex = myStartEntryIndices[index]; MyDataOffset = myStartEntryOffsets[index]; myDataOffset = myStartEntryOffsets[index]; }Copy the code

Next, since the paragraph type is TEXT_PARAGRAPH, new Processor (…) is executed. The fill () :

Void fill() {// Ignore some code... 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: / / ignore the part of the code... break; Case ZLTextParagraph. Entry. HYPERLINK_CONTROL: / / ignore the part of the code... 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. The EXTENSION: / / ignore the part of the code... 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; }}}Copy the code

This will enter a for loop with the condition it.next(), which is myparagraph.iterator (), which we analyzed in the previous step. Iterator returns an EntryIteratorImpl, so look at the next method of EntryIteratorImpl:

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: {/ / ignore the part of the code... break; } case ZLTextParagraph. Entry. HYPERLINK_CONTROL: {/ / ignore the part of the code... 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: / / ignore the part of the code... break; Case ZLTextParagraph. Entry. STYLE_CSS: case ZLTextParagraph. Entry. STYLE_OTHER: {/ / ignore the part of the code... } 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: {/ / ignore the part of the code... break; } case ZLTextParagraph. Entry. The EXTENSION: {/ / ignore the part of the code... break; } } ++myCounter; myDataOffset = dataOffset; return true; }Copy the code

In the next method, there is a role CachedCharStorage that was analyzed earlier, and its block method is called first:

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; }Copy the code

When you call the block method, you pass in myDataIndex, which specifies which NCAHCE file the contents of the current paragraph are in. It is not difficult to analyze the main functions of the Next method:

  • Read the ncache of the segment to be fetched. If CachedCharStorage is already cached, fetch the cache. Otherwise, read the corresponding ncache file directly
  • Read the next ncache file if necessary (the current paragraph starts in x.ncache and ends in x+1.ncahce)
  • According to the paragraph content length read by Native, call next to read one content element each time, and record the element type (possibly TEXT, IMAGE and other formats), data content, offset and length read

Here, we go back to the for loop again. As we already know from the next method, it reads an element and stores the type of the element it reads. If we look at the code inside the for loop, we can see that it then assembles the data according to the type of the element it reads. Finally, it is saved to the ArrayList collection of ZLTextParagraphCursor. That is, each element of the index paragraph is finally read by this fill method and stored in the collection.

3. Pull the required content data through the “gate” of the data warehouse and draw the corresponding bitmap of the page

When we initialize our ZLTextParagraphCursor, we already know that it parses its content through the fill method. At this point, let’s go back to the setModel method:

Public synchronized void setModel(ZLTextModel) { if (myModel ! = null) { final int paragraphsNumber = myModel.getParagraphsNumber(); if (paragraphsNumber > 0) { myCurrentPage.moveStartCursor(myCursorManager.get(0)); }} // Ignore some code... }Copy the code

Moves the startCursor of the current page to the first paragraph and sets the PaintState of the current page to START_IS_KNOWN. At this point the page is ready for the “starting gun”! When was the starting gun fired? This brings me back to my old friend, the ZLAndroidWidget, the only control in the FBReader interface. Its onDraw method, which we have already analyzed, calls onDrawStatic when at rest:

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);
}
Copy the code

Zlapplication.instance ().getCurrentView() returns the same object as setModel’s BookTextView, and its paint method is called:

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(); }} // Ignore some code... }Copy the code

1. It will get the wallpaper that is currently set. If it can get the wallpaper, it will get the drawing method of the wallpaper.

2. Obtain the page object based on the page Index.

3. After obtaining the page object to be drawn, preparePaintInfo method is used to construct the basic element information of the page according to the PaintState of the current page. This method will set the page size (width and height of the drawable area and whether it is drawn in double columns, etc.).

private synchronized void preparePaintInfo(ZLTextPage page) { page.setSize(getTextColumnWidth(), getTextAreaHeight(), twoColumnView(), page == myPreviousPage); // Ignore some code... 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: // Ignore some code... break; Case PaintStateEnum.TO_SCROLL_BACKWARD: // Ignore some code... break; case PaintStateEnum.START_IS_KNOWN: if (! page.StartCursor.isNull()) { buildInfos(page, page.StartCursor, page.EndCursor); } break; Case PaintStateEnum.END_IS_KNOWN: // Ignore some code... 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(); }}}Copy the code

4. According to the previous analysis, the PaintState of the current page is set to START_IS_KNOWN at moveStartCursor, so the buildInfos method is called to build the original data of the page:

private void buildInfos(ZLTextPage page, ZLTextWordCursor start, ZLTextWordCursor result) { result.setCursor(start); Startcursor int textAreaHeight = page.gettextheight (); Page.lineinfos.clear (); // Get the height of the drawable content area on the current page. // Clear the previous build information page.Column0Height = 0; Boolean nextParagraph; // Whether the next paragraph is ZLTextLineInfo info = null; Do {final ZLTextLineInfo previousInfo = info; resetTextStyle(); final ZLTextParagraphCursor paragraphCursor result.getParagraphCursor(); Cursor final int wordIndex = result.getelementIndex (); // Start index applyStyleChanges(paragraphCursor, 0, wordIndex); info = new ZLTextLineInfo(paragraphCursor, wordIndex, result.getCharIndex(), getTextStyle()); / / build a line information final int endIndex = info. ParagraphCursorLength; // End index (paragraph content length) 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 (); 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){ }Copy the code

Using the buildInfos method as an example of a build scenario when you have read it for the first time, the following things are done for the page where the content is to be built:

  • Page’s startCusor was previously moved to the first paragraph, and the first paragraph was read at creation time. In this method, the paragraph content elements that have been read are iterated over
  • In the process of traversing the elements, travel element information will be constructed line by line according to the width of the drawable area, and the height of each line is the height of the element with the highest height in the row
  • Each produced row of elements is then judged to be able to be added to the page based on the height of the drawable area. If so, join and continue building the next line; If not, exit the build and the current page element is built
  • If, for the first paragraph, each element is traversed and the height is still available after cutting the row elements of each row, the next paragraph is obtained and the above steps are repeated to build the row information until the completion of the construction

At this point, the content data of the current page has been constructed, line by line, based on the actual available space. Each row contains the data element that was read earlier.

5. Wrap elements and turn them into “regions” of elements that can be drawn by Cavas

After the page data build above, the data content of the page in its current state has been built line by line. However, the data currently built is just data, and our ultimate goal is to generate a bitmap of the page. Then each element of each row needs to be described in a location description, transformed into a page of content with real location and data information. The transition is done by iterating through each line with for:

For (ZLTextLineInfo info: lineInfos) {info.adjust(previousInfo); PrepareTextLine (Page, info, x, y, columnIndex); 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; }Copy the code

6. Draw each row element “region” for each row

The element “region” is wrapped and ready to be drawn:

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) {// Ignore some code... } else if (element instanceof ExtensionElement) {// Ignore some code... Element} else if (= = ZLTextElement. Img tags like HSpace | | element. = = ZLTextElement NBSpace) {/ / ignore the part of the code... }}} // Ignore some code... }Copy the code

7. Draw executor — ZLAndroidPaintContext

The final drawing, which is performed by such objects, has two main methods:

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); }}Copy the code

8. Compare bitmap content before and after paint method

At first bitmap:

After the paint method completes, the bitmap:

At this point, the bitmap for the current page is ready to complete. This bitmap is passed to the ZLAndroidWidget through the bitmapmanager and is eventually drawn onto the control.

Of course, due to my limited time in contact with this project, and my lack of experience in writing technical articles, there will inevitably be mistakes, unclear descriptions, language overload and other problems in the process, I hope you can understand, but also hope that you continue to give correction. Finally, THANK you for your support to me, so that I have a strong motivation to stick to it.