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

Commit e7422024 authored by Haoyu Zhang's avatar Haoyu Zhang
Browse files

Introduce Layout#fillCharacterBounds

Introduce Layout#fillCharacterBounds to compute character bounds faster.
This change makes TextView#populateCharacter 5 times faster.

After:
        populateCharacterBounds[mTextLength (100))]_min (ns): 52361
        populateCharacterBounds[mTextLength (300))]_min (ns): 148941
        populateCharacterBounds[mTextLength (1,000))]_min (ns): 463237
        populateCharacterBounds[mTextLength (3,000))]_min (ns): 1388081
        populateCharacterBounds[mTextLength (10,000))]_min (ns): 1165867

Before:
        populateCharacterBounds[mTextLength (100))]_min (ns): 204065
        populateCharacterBounds[mTextLength (300))]_min (ns): 653159
        populateCharacterBounds[mTextLength (1,000))]_min (ns): 2184327
        populateCharacterBounds[mTextLength (3,000))]_min (ns): 6770318
        populateCharacterBounds[mTextLength (10,000))]_min (ns): 22777530

Bug: 233922052
Test: atest android.text.TextViewPopulateCharacterBoundsTest
Test: atest android.text.TextViewCursorAnchorInfoPerfTest
Change-Id: I68a47cf3099b89e0a10604b7706fa7a3dea66d32
parent 6cdf9112
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -45090,6 +45090,7 @@ package android.text {
    ctor protected Layout(CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float);
    method public void draw(android.graphics.Canvas);
    method public void draw(android.graphics.Canvas, android.graphics.Path, android.graphics.Paint, int);
    method public void fillCharacterBounds(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull float[], @IntRange(from=0) int);
    method public final android.text.Layout.Alignment getAlignment();
    method public abstract int getBottomPadding();
    method public void getCursorPath(int, android.graphics.Path, CharSequence);
+96 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.text;

import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Canvas;
import android.graphics.Paint;
@@ -1311,6 +1312,101 @@ public abstract class Layout {
        return horizontal;
    }

    /**
     * Return the characters' bounds in the given range. The {@code bounds} array will be filled
     * starting from {@code boundsStart} (inclusive). The coordinates are in local text layout.
     *
     * @param start the start index to compute the character bounds, inclusive.
     * @param end the end index to compute the character bounds, exclusive.
     * @param bounds the array to fill in the character bounds. The array is divided into segments
     *               of four where each index in that segment represents left, top, right and
     *               bottom of the character.
     * @param boundsStart the inclusive start index in the array to start filling in the values
     *                    from.
     *
     * @throws IndexOutOfBoundsException if the range defined by {@code start} and {@code end}
     * exceeds the range of the text, or {@code bounds} doesn't have enough space to store the
     * result.
     * @throws IllegalArgumentException if {@code bounds} is null.
     */
    public void fillCharacterBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
            @NonNull float[] bounds, @IntRange(from = 0) int boundsStart) {
        if (start < 0 || end < start || end > mText.length()) {
            throw new IndexOutOfBoundsException("given range: " + start + ", " + end + " is "
                    + "out of the text range: 0, " + mText.length());
        }

        if (bounds == null) {
            throw  new IllegalArgumentException("bounds can't be null.");
        }

        final int neededLength = 4 * (end - start);
        if (neededLength > bounds.length - boundsStart) {
            throw new IndexOutOfBoundsException("bounds doesn't have enough space to store the "
                    + "result, needed: " + neededLength + " had: "
                    + (bounds.length - boundsStart));
        }

        if (start == end) {
            return;
        }

        final int startLine = getLineForOffset(start);
        final int endLine = getLineForOffset(end - 1);
        float[] horizontalBounds = null;
        for (int line = startLine; line <= endLine; ++line) {
            final int lineStart = getLineStart(line);
            final int lineEnd = getLineEnd(line);
            final int lineLength = lineEnd - lineStart;

            final int dir = getParagraphDirection(line);
            final boolean hasTab = getLineContainsTab(line);
            final Directions directions = getLineDirections(line);

            TabStops tabStops = null;
            if (hasTab && mText instanceof Spanned) {
                // Just checking this line should be good enough, tabs should be
                // consistent across all lines in a paragraph.
                TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, lineStart, lineEnd,
                        TabStopSpan.class);
                if (tabs.length > 0) {
                    tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
                }
            }

            final TextLine tl = TextLine.obtain();
            tl.set(mPaint, mText, lineStart, lineEnd, dir, directions, hasTab, tabStops,
                    getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
                    isFallbackLineSpacingEnabled());
            if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) {
                horizontalBounds = new float[2 * lineLength];
            }

            tl.measureAllBounds(horizontalBounds, null);
            TextLine.recycle(tl);
            final int lineLeft = getParagraphLeft(line);
            final int lineRight = getParagraphRight(line);
            final int lineStartPos = getLineStartPos(line, lineLeft, lineRight);

            final int lineTop = getLineTop(line);
            final int lineBottom = getLineBottom(line);

            final int startIndex = Math.max(start, lineStart);
            final int endIndex = Math.min(end, lineEnd);
            for (int index = startIndex; index < endIndex; ++index) {
                final int offset = index - lineStart;
                final float left = horizontalBounds[offset * 2] + lineStartPos;
                final float right = horizontalBounds[offset * 2 + 1] + lineStartPos;

                final int boundsIndex = boundsStart + 4 * (index - start);
                bounds[boundsIndex] = left;
                bounds[boundsIndex + 1] = lineTop;
                bounds[boundsIndex + 2] = right;
                bounds[boundsIndex + 3] = lineBottom;
            }
        }
    }

    /**
     * Get the leftmost position that should be exposed for horizontal
     * scrolling on the specified line.
+29 −55
Original line number Diff line number Diff line
@@ -12530,64 +12530,38 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
    public void populateCharacterBounds(CursorAnchorInfo.Builder builder,
            int startIndex, int endIndex, float viewportToContentHorizontalOffset,
            float viewportToContentVerticalOffset) {
        final int minLine = mLayout.getLineForOffset(startIndex);
        final int maxLine = mLayout.getLineForOffset(endIndex - 1);
        final Rect rect = new Rect();
        getLocalVisibleRect(rect);
        final RectF visibleRect = new RectF(rect);
        for (int line = minLine; line <= maxLine; ++line) {
            final int lineStart = mLayout.getLineStart(line);
            final int lineEnd = mLayout.getLineEnd(line);
            final int offsetStart = Math.max(lineStart, startIndex);
            final int offsetEnd = Math.min(lineEnd, endIndex);
            final boolean ltrLine =
                    mLayout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
            final float[] widths = new float[offsetEnd - offsetStart];
            mLayout.getPaint().getTextWidths(mTransformed, offsetStart, offsetEnd, widths);
            final float top = mLayout.getLineTop(line);
            final float bottom = mLayout.getLineBottom(line);
            for (int offset = offsetStart; offset < offsetEnd; ++offset) {
                final float charWidth = widths[offset - offsetStart];
                final boolean isRtl = mLayout.isRtlCharAt(offset);
                // TODO: This doesn't work perfectly for text with custom styles and
                // TAB chars.
                final float left;
                if (ltrLine) {
                    if (isRtl) {
                        left = mLayout.getSecondaryHorizontal(offset) - charWidth;
                    } else {
                        left = mLayout.getPrimaryHorizontal(offset);
                    }
                } else {
                    if (!isRtl) {
                        left = mLayout.getSecondaryHorizontal(offset);
                    } else {
                        left = mLayout.getPrimaryHorizontal(offset) - charWidth;
                    }
                }
                final float right = left + charWidth;
                // TODO: Check top-right and bottom-left as well.
                final float localLeft = left + viewportToContentHorizontalOffset;
                final float localRight = right + viewportToContentHorizontalOffset;
                final float localTop = top + viewportToContentVerticalOffset;
                final float localBottom = bottom + viewportToContentVerticalOffset;
                final boolean isTopLeftVisible = visibleRect.contains(localLeft, localTop);
                final boolean isBottomRightVisible =
                        visibleRect.contains(localRight, localBottom);
        final float[] characterBounds = new float[4 * (endIndex - startIndex)];
        mLayout.fillCharacterBounds(startIndex, endIndex, characterBounds, 0);
        final int limit = endIndex - startIndex;
        for (int offset = 0; offset < limit; ++offset) {
            final float left =
                    characterBounds[offset * 4] + viewportToContentHorizontalOffset;
            final float top =
                    characterBounds[offset * 4 + 1] + viewportToContentVerticalOffset;
            final float right =
                    characterBounds[offset * 4 + 2] + viewportToContentHorizontalOffset;
            final float bottom =
                    characterBounds[offset * 4 + 3] + viewportToContentVerticalOffset;
            final boolean hasVisibleRegion = visibleRect.intersects(left, top, right, bottom);
            final boolean hasInVisibleRegion = !visibleRect.contains(left, top, right, bottom);
            int characterBoundsFlags = 0;
                if (isTopLeftVisible || isBottomRightVisible) {
            if (hasVisibleRegion) {
                characterBoundsFlags |= FLAG_HAS_VISIBLE_REGION;
            }
                if (!isTopLeftVisible || !isBottomRightVisible) {
            if (hasInVisibleRegion) {
                characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
            }
                if (isRtl) {
            if (mLayout.isRtlCharAt(offset)) {
                characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
            }
                // Here offset is the index in Java chars.
                builder.addCharacterBounds(offset, localLeft, localTop, localRight,
                        localBottom, characterBoundsFlags);
            }
            builder.addCharacterBounds(offset + startIndex, left, top, right, bottom,
                    characterBoundsFlags);
        }
    }
+1 −0
Original line number Diff line number Diff line
@@ -144,6 +144,7 @@
      <map code="0x0056" name="5em" />  <!-- V -->
      <map code="0x0058" name="10em" />  <!-- X -->
      <map code="0x005f" name="0em" /> <!-- _ -->
      <map code="0x000a" name="0em" /> <!-- NEW_LINE -->
      <map code="0x05D0" name="1em" /> <!-- HEBREW LETTER ALEF -->
      <map code="0x05D1" name="5em" /> <!-- HEBREW LETTER BET -->
      <map code="0xfffd" name="7em" /> <!-- REPLACEMENT CHAR -->
Loading