Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 33b7de85 authored by Gilles Debunne's avatar Gilles Debunne
Browse files

Multiple display lists for editable text

Bug 5763685

Long text in a ScrollView (not when the View's internal
scroll is used) is cached as a unique display list when hardware
rendering is on.

As a result, each time the text is edited, the entire display
list has to be updated, which takes a significant amount of
time (up to 500ms for a few thousand lines), proportional to the
size of the text.

This CL splits the text into multiple display lists as the
text is edited. The boundaries of the display list are aligned
with paragraphs.

There is still an issue when the number of lines changes: onLayout()
is called which invalidates all the display list. When the source
of that change is line wrapping and not a change in the view's
dimensions, we should be able to simply shift down the previous DL
instead of re-creating everything.

Change-Id: I7de49a1e5637cdfc9ef06b64b1ec4b61d9ea2415
parent 58984b07
Loading
Loading
Loading
Loading
+142 −3
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@ import android.graphics.Paint;
import android.text.style.UpdateLayout;
import android.text.style.WrapTogetherSpan;

import com.android.internal.util.ArrayUtils;

import java.lang.ref.WeakReference;

/**
@@ -30,8 +32,7 @@ import java.lang.ref.WeakReference;
 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
 *  Canvas.drawText()} directly.</p>
 */
public class DynamicLayout
extends Layout
public class DynamicLayout extends Layout
{
    private static final int PRIORITY = 128;

@@ -116,6 +117,10 @@ extends Layout

        mObjects = new PackedObjectVector<Directions>(1);

        mBlockEnds = new int[] { 0 };
        mBlockIndices = new int[] { INVALID_BLOCK_INDEX };
        mNumberOfBlocks = 1;

        mIncludePad = includepad;

        /*
@@ -295,9 +300,9 @@ extends Layout
            n--;

        // remove affected lines from old layout

        mInts.deleteAt(startline, endline - startline);
        mObjects.deleteAt(startline, endline - startline);
        updateBlocks(startline, endline - 1, n);

        // adjust offsets in layout for new height and offsets

@@ -363,6 +368,124 @@ extends Layout
        }
    }

    /**
     * This method is called every time the layout is reflowed after an edition.
     * It updates the internal block data structure. The text is split in blocks
     * of contiguous lines, with at least one block for the entire text.
     * When a range of lines is edited, new blocks (from 0 to 3 depending on the
     * overlap structure) will replace the set of overlapping blocks.
     * Blocks are listed in order and are represented by their ending line number.
     * An index is associated to each block (which will be used by display lists),
     * this class simply invalidates the index of blocks overlapping a modification.
     *
     * @param startLine the first line of the range of modified lines
     * @param endLine the last line of the range, possibly equal to startLine, lower
     * than getLineCount()
     * @param newLineCount the number of lines that will replace the range, possibly 0
     */
    private void updateBlocks(int startLine, int endLine, int newLineCount) {
        int firstBlock = -1;
        int lastBlock = -1;
        for (int i = 0; i < mNumberOfBlocks; i++) {
            if (mBlockEnds[i] >= startLine) {
                firstBlock = i;
                break;
            }
        }
        for (int i = firstBlock; i < mNumberOfBlocks; i++) {
            if (mBlockEnds[i] >= endLine) {
                lastBlock = i;
                break;
            }
        }
        final int lastBlockEndLine = mBlockEnds[lastBlock];

        boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
                mBlockEnds[firstBlock - 1] + 1);
        boolean createBlock = newLineCount > 0;
        boolean createBlockAfter = endLine < mBlockEnds[lastBlock];

        int numAddedBlocks = 0;
        if (createBlockBefore) numAddedBlocks++;
        if (createBlock) numAddedBlocks++;
        if (createBlockAfter) numAddedBlocks++;

        final int numRemovedBlocks = lastBlock - firstBlock + 1;
        final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;

        if (newNumberOfBlocks == 0) {
            // Even when text is empty, there is actually one line and hence one block
            mBlockEnds[0] = 0;
            mBlockIndices[0] = INVALID_BLOCK_INDEX;
            mNumberOfBlocks = 1;
            return;
        }

        if (newNumberOfBlocks > mBlockEnds.length) {
            final int newSize = ArrayUtils.idealIntArraySize(newNumberOfBlocks);
            int[] blockEnds = new int[newSize];
            int[] blockIndices = new int[newSize];
            System.arraycopy(mBlockEnds, 0, blockEnds, 0, firstBlock);
            System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
            System.arraycopy(mBlockEnds, lastBlock + 1,
                    blockEnds, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
            System.arraycopy(mBlockIndices, lastBlock + 1,
                    blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
            mBlockEnds = blockEnds;
            mBlockIndices = blockIndices;
        } else {
            System.arraycopy(mBlockEnds, lastBlock + 1,
                    mBlockEnds, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
            System.arraycopy(mBlockIndices, lastBlock + 1,
                    mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
        }

        mNumberOfBlocks = newNumberOfBlocks;
        final int deltaLines = newLineCount - (endLine - startLine + 1);
        for (int i = firstBlock + numAddedBlocks; i < mNumberOfBlocks; i++) {
            mBlockEnds[i] += deltaLines;
        }

        int blockIndex = firstBlock;
        if (createBlockBefore) {
            mBlockEnds[blockIndex] = startLine - 1;
            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
            blockIndex++;
        }

        if (createBlock) {
            mBlockEnds[blockIndex] = startLine + newLineCount - 1;
            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
            blockIndex++;
        }

        if (createBlockAfter) {
            mBlockEnds[blockIndex] = lastBlockEndLine + deltaLines;
            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
        }
    }

    /**
     * @hide
     */
    public int[] getBlockEnds() {
        return mBlockEnds;
    }

    /**
     * @hide
     */
    public int[] getBlockIndices() {
        return mBlockIndices;
    }

    /**
     * @hide
     */
    public int getNumberOfBlocks() {
        return mNumberOfBlocks;
    }

    @Override
    public int getLineCount() {
        return mInts.size() - 1;
@@ -428,6 +551,7 @@ extends Layout
        }

        public void beforeTextChanged(CharSequence s, int where, int before, int after) {
            // Intentionally empty
        }

        public void onTextChanged(CharSequence s, int where, int before, int after) {
@@ -435,6 +559,7 @@ extends Layout
        }

        public void afterTextChanged(Editable s) {
            // Intentionally empty
        }

        public void onSpanAdded(Spannable s, Object o, int start, int end) {
@@ -486,6 +611,20 @@ extends Layout
    private PackedIntVector mInts;
    private PackedObjectVector<Directions> mObjects;

    /*
     * Value used in mBlockIndices when a block has been created or recycled and indicating that its
     * display list needs to be re-created.
     * @hide
     */
    public static final int INVALID_BLOCK_INDEX = -1;
    // Stores the line numbers of the last line of each block
    private int[] mBlockEnds;
    // The indices of this block's display list in TextView's internal display list array or
    // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
    private int[] mBlockIndices;
    // Number of items actually currently being used in the above 2 arrays
    private int mNumberOfBlocks;

    private int mTopPadding, mBottomPadding;

    private static StaticLayout sStaticLayout = new StaticLayout(null);
+90 −28
Original line number Diff line number Diff line
@@ -139,6 +139,7 @@ import android.view.textservice.TextServicesManager;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.RemoteViews.RemoteView;

import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastMath;
import com.android.internal.widget.EditableInputConnection;

@@ -1214,6 +1215,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            if (imm != null) imm.restartInput(this);
        }

        // Will change text color
        if (mEditor != null) getEditor().invalidateTextDisplayList();
        prepareCursorControllers();

@@ -2328,7 +2330,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
    public void setHighlightColor(int color) {
        if (mHighlightColor != color) {
            mHighlightColor = color;
            if (mEditor != null) getEditor().invalidateTextDisplayList();
            invalidate();
        }
    }
@@ -2349,6 +2350,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        mShadowDx = dx;
        mShadowDy = dy;

        // Will change text clip region
        if (mEditor != null) getEditor().invalidateTextDisplayList();
        invalidate();
    }
@@ -2841,6 +2843,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            }
        }
        if (inval) {
            // Text needs to be redrawn with the new color
            if (mEditor != null) getEditor().invalidateTextDisplayList();
            invalidate();
        }
@@ -3332,7 +3335,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            invalidate();
        }

        // Invalidate display list if hint will be used
        // Invalidate display list if hint is currently used
        if (mEditor != null && mText.length() == 0 && mHint != null) {
            getEditor().invalidateTextDisplayList();
        }
@@ -8274,6 +8277,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            if (getEditor().mPositionListener != null) {
                getEditor().mPositionListener.onScrollChanged();
            }
            // Internal scroll affects the clip boundaries
            getEditor().invalidateTextDisplayList();
        }
    }
@@ -11299,7 +11303,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        InputContentType mInputContentType;
        InputMethodState mInputMethodState;

        DisplayList mTextDisplayList;
        DisplayList[] mTextDisplayLists;

        boolean mFrozenWithFocus;
        boolean mSelectionMoved;
@@ -11545,7 +11549,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener

        void sendOnTextChanged(int start, int after) {
            updateSpellCheckSpans(start, start + after, false);
            invalidateTextDisplayList();

            // Hide the controllers as soon as text is modified (typing, procedural...)
            // We do not hide the span controllers, since they can be added when a new text is
@@ -11702,36 +11705,91 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            layout.drawBackground(canvas, highlight, mHighlightPaint, cursorOffsetVertical,
                    firstLine, lastLine);

            if (mTextDisplayList == null || !mTextDisplayList.isValid()) {
                boolean displayListCreated = false;
                if (mTextDisplayList == null) {
                    mTextDisplayList = getHardwareRenderer().createDisplayList("Text");
                    displayListCreated = true;
            if (mTextDisplayLists == null) {
                mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
            }
            if (! (layout instanceof DynamicLayout)) {
                Log.e(LOG_TAG, "Editable TextView is not using a DynamicLayout");
                return;
            }

            DynamicLayout dynamicLayout = (DynamicLayout) layout;
            int[] blockEnds = dynamicLayout.getBlockEnds();
            int[] blockIndices = dynamicLayout.getBlockIndices();
            final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();

            canvas.translate(mScrollX, mScrollY);
            int endOfPreviousBlock = -1;
            int searchStartIndex = 0;
            for (int i = 0; i < numberOfBlocks; i++) {
                int blockEnd = blockEnds[i];
                int blockIndex = blockIndices[i];

                final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
                if (blockIsInvalid) {
                    blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
                            searchStartIndex);
                    // Dynamic layout internal block indices structure is updated from Editor
                    blockIndices[i] = blockIndex;
                    searchStartIndex = blockIndex + 1;
                }

                DisplayList blockDisplayList = mTextDisplayLists[blockIndex];
                if (blockDisplayList == null) {
                    blockDisplayList = mTextDisplayLists[blockIndex] =
                            getHardwareRenderer().createDisplayList("Text " + blockIndex);
                } else {
                    if (blockIsInvalid) blockDisplayList.invalidate();
                }

                final HardwareCanvas hardwareCanvas = mTextDisplayList.start();
                if (!blockDisplayList.isValid()) {
                    final HardwareCanvas hardwareCanvas = blockDisplayList.start();
                    try {
                        hardwareCanvas.setViewport(width, height);
                        // The dirty rect should always be null for a display list
                        hardwareCanvas.onPreDraw(null);
                        hardwareCanvas.translate(-mScrollX, -mScrollY);
                    layout.drawText(hardwareCanvas, firstLine, lastLine);
                    //layout.draw(hardwareCanvas, highlight, mHighlightPaint, cursorOffsetVertical);
                        layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd);
                        hardwareCanvas.translate(mScrollX, mScrollY);
                    } finally {
                        hardwareCanvas.onPostDraw();
                    mTextDisplayList.end();
                    if (displayListCreated && USE_DISPLAY_LIST_PROPERTIES) {
                        mTextDisplayList.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
                        blockDisplayList.end();
                        if (USE_DISPLAY_LIST_PROPERTIES) {
                            blockDisplayList.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
                        }
                    }
                }
            canvas.translate(mScrollX, mScrollY);
            ((HardwareCanvas) canvas).drawDisplayList(mTextDisplayList, width, height, null,

                ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null,
                        DisplayList.FLAG_CLIP_CHILDREN);
                endOfPreviousBlock = blockEnd;
            }
            canvas.translate(-mScrollX, -mScrollY);
        }

        private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
                int searchStartIndex) {
            int length = mTextDisplayLists.length;
            for (int i = searchStartIndex; i < length; i++) {
                boolean blockIndexFound = false;
                for (int j = 0; j < numberOfBlocks; j++) {
                    if (blockIndices[j] == i) {
                        blockIndexFound = true;
                        break;
                    }
                }
                if (blockIndexFound) continue;
                return i;
            }

            // No available index found, the pool has to grow
            int newSize = ArrayUtils.idealIntArraySize(length + 1);
            DisplayList[] displayLists = new DisplayList[newSize];
            System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length);
            mTextDisplayLists = displayLists;
            return length;
        }

        private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
            final boolean translate = cursorOffsetVertical != 0;
            if (translate) canvas.translate(0, cursorOffsetVertical);
@@ -11742,7 +11800,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        }

        private void invalidateTextDisplayList() {
            if (mTextDisplayList != null) mTextDisplayList.invalidate();
            if (mTextDisplayLists != null) {
                for (int i = 0; i < mTextDisplayLists.length; i++) {
                    if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate();
                }
            }
        }

        private void updateCursorsPositions() {