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

Commit 50858c10 authored by Haoyu Zhang's avatar Haoyu Zhang Committed by Android (Google) Code Review
Browse files

Merge "Support TextBoundsInfo for TextView"

parents 27b6cf7a bfdf3ddb
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ public class GraphemeClusterSegmentFinder extends SegmentFinder {

    @Override
    public int previousStartBoundary(@IntRange(from = 0) int offset) {
        if (offset == 0) return DONE;
        int boundary = mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE);
        return boundary == -1 ? DONE : boundary;
@@ -56,6 +57,7 @@ public class GraphemeClusterSegmentFinder extends SegmentFinder {

    @Override
    public int previousEndBoundary(@IntRange(from = 0) int offset) {
        if (offset == 0) return DONE;
        int boundary = mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE);
        // Check that there is another cursor position before, otherwise this is not a valid
@@ -69,6 +71,7 @@ public class GraphemeClusterSegmentFinder extends SegmentFinder {

    @Override
    public int nextStartBoundary(@IntRange(from = 0) int offset) {
        if (offset == mText.length()) return DONE;
        int boundary = mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER);
        // Check that there is another cursor position after, otherwise this is not a valid
@@ -82,6 +85,7 @@ public class GraphemeClusterSegmentFinder extends SegmentFinder {

    @Override
    public int nextEndBoundary(@IntRange(from = 0) int offset) {
        if (offset == mText.length()) return DONE;
        int boundary = mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER);
        return boundary == -1 ? DONE : boundary;
+12 −0
Original line number Diff line number Diff line
@@ -2999,6 +2999,18 @@ public abstract class Layout {
            return mDirections[runIndex * 2 + 1] & RUN_LENGTH_MASK;
        }

        /**
         * Returns the BiDi level of this run.
         *
         * @param runIndex the index of the BiDi run
         * @return the BiDi level of this run.
         * @hide
         */
        @IntRange(from = 0)
        public int getRunLevel(int runIndex) {
            return (mDirections[runIndex * 2 + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
        }

        /**
         * Returns true if the BiDi run is RTL.
         *
+2 −1
Original line number Diff line number Diff line
@@ -2331,7 +2331,8 @@ public class TextUtils {
        return trimmed;
    }

    private static boolean isNewline(int codePoint) {
    /** @hide */
    public static boolean isNewline(int codePoint) {
        int type = Character.getType(codePoint);
        return type == Character.PARAGRAPH_SEPARATOR || type == Character.LINE_SEPARATOR
                || codePoint == LINE_FEED_CODE_POINT;
+152 −10
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import android.graphics.BaseCanvas;
import android.graphics.BlendMode;
import android.graphics.Canvas;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Path;
@@ -200,6 +201,7 @@ import android.view.inputmethod.JoinOrSplitGesture;
import android.view.inputmethod.RemoveSpaceGesture;
import android.view.inputmethod.SelectGesture;
import android.view.inputmethod.SelectRangeGesture;
import android.view.inputmethod.TextBoundsInfo;
import android.view.inspector.InspectableProperty;
import android.view.inspector.InspectableProperty.EnumEntry;
import android.view.inspector.InspectableProperty.FlagEntry;
@@ -12953,18 +12955,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        getLocalVisibleRect(rect);
        final RectF visibleRect = new RectF(rect);
        final float[] characterBounds = new float[4 * (endIndex - startIndex)];
        mLayout.fillCharacterBounds(startIndex, endIndex, characterBounds, 0);
        final float[] characterBounds = getCharacterBounds(startIndex, endIndex,
                viewportToContentHorizontalOffset, viewportToContentVerticalOffset);
        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 float left = characterBounds[offset * 4];
            final float top = characterBounds[offset * 4 + 1];
            final float right = characterBounds[offset * 4 + 2];
            final float bottom = characterBounds[offset * 4 + 3];
            final boolean hasVisibleRegion = visibleRect.intersects(left, top, right, bottom);
            final boolean hasInVisibleRegion = !visibleRect.contains(left, top, right, bottom);
@@ -12984,6 +12983,149 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        }
    }
    /**
     * Return the bounds of the characters in the given range, in TextView's coordinates.
     *
     * @param start the start index of the interested text range, inclusive.
     * @param end the end index of the interested text range, exclusive.
     * @param layoutLeft the left of the given {@code layout} in the editor view's coordinates.
     * @param layoutTop  the top of the given {@code layout} in the editor view's coordinates.
     * @return the character bounds stored in a flattened array, in the editor view's coordinates.
     */
    private float[] getCharacterBounds(int start, int end, float layoutLeft, float layoutTop) {
        final float[] characterBounds = new float[4 * (end - start)];
        mLayout.fillCharacterBounds(start, end, characterBounds, 0);
        for (int offset = 0; offset < end - start; ++offset) {
            characterBounds[4 * offset] += layoutLeft;
            characterBounds[4 * offset + 1] += layoutTop;
            characterBounds[4 * offset + 2] += layoutLeft;
            characterBounds[4 * offset + 3] += layoutTop;
        }
        return characterBounds;
    }
    /**
     * Creates the {@link TextBoundsInfo} for the text lines that intersects with the {@code rectF}.
     * @hide
     */
    public TextBoundsInfo getTextBoundsInfo(@NonNull RectF rectF) {
        final Layout layout = getLayout();
        if (layout == null) {
            // No valid text layout, return null.
            return null;
        }
        final CharSequence text = layout.getText();
        if (text == null) {
            // It's impossible that a layout has no text. Check here to avoid NPE.
            return null;
        }
        final Matrix localToGlobalMatrix = new Matrix();
        transformMatrixToGlobal(localToGlobalMatrix);
        final Matrix globalToLocalMatrix = new Matrix();
        if (!localToGlobalMatrix.invert(globalToLocalMatrix)) {
            // Can't map global rectF to local coordinates, this is almost impossible in practice.
            return null;
        }
        final float layoutLeft = viewportToContentHorizontalOffset();
        final float layoutTop = viewportToContentVerticalOffset();
        final RectF localRectF = new RectF(rectF);
        globalToLocalMatrix.mapRect(localRectF);
        localRectF.offset(-layoutLeft, -layoutTop);
        // Text length is 0. There is no character bounds, return empty TextBoundsInfo.
        // rectF doesn't intersect with the layout, return empty TextBoundsInfo.
        if (!localRectF.intersects(0f, 0f, layout.getWidth(), layout.getHeight())
                || text.length() == 0) {
            final TextBoundsInfo.Builder builder = new TextBoundsInfo.Builder();
            final SegmentFinder emptySegmentFinder =
                    new SegmentFinder.DefaultSegmentFinder(new int[0]);
            builder.setStartAndEnd(0, 0)
                    .setMatrix(localToGlobalMatrix)
                    .setCharacterBounds(new float[0])
                    .setCharacterBidiLevel(new int[0])
                    .setCharacterFlags(new int[0])
                    .setGraphemeSegmentFinder(emptySegmentFinder)
                    .setLineSegmentFinder(emptySegmentFinder)
                    .setWordSegmentFinder(emptySegmentFinder);
            return  builder.build();
        }
        final int startLine = layout.getLineForVertical((int) Math.floor(localRectF.top));
        final int endLine = layout.getLineForVertical((int) Math.floor(localRectF.bottom));
        final int start = layout.getLineStart(startLine);
        final int end = layout.getLineEnd(endLine);
        // Compute character bounds.
        final float[] characterBounds = getCharacterBounds(start, end, layoutLeft, layoutTop);
        // Compute character flags and BiDi levels.
        final int[] characterFlags = new int[end - start];
        final int[] characterBidiLevels = new int[end - start];
        for (int line = startLine; line <= endLine; ++line) {
            final int lineStart = layout.getLineStart(line);
            final int lineEnd = layout.getLineEnd(line);
            final Layout.Directions directions = layout.getLineDirections(line);
            for (int i = 0; i < directions.getRunCount(); ++i) {
                final int runStart = directions.getRunStart(i) + lineStart;
                final int runEnd = Math.min(runStart + directions.getRunLength(i), lineEnd);
                final int runLevel = directions.getRunLevel(i);
                Arrays.fill(characterBidiLevels, runStart - start, runEnd - start, runLevel);
            }
            final boolean lineIsRtl =
                    layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
            for (int index = lineStart; index < lineEnd; ++index) {
                int flags = 0;
                if (TextUtils.isWhitespace(text.charAt(index))) {
                    flags |= TextBoundsInfo.FLAG_CHARACTER_WHITESPACE;
                }
                if (TextUtils.isPunctuation(Character.codePointAt(text, index))) {
                    flags |= TextBoundsInfo.FLAG_CHARACTER_PUNCTUATION;
                }
                if (TextUtils.isNewline(Character.codePointAt(text, index))) {
                    flags |= TextBoundsInfo.FLAG_CHARACTER_LINEFEED;
                }
                if (lineIsRtl) {
                    flags |= TextBoundsInfo.FLAG_LINE_IS_RTL;
                }
                characterFlags[index - start] = flags;
            }
        }
        // Create grapheme SegmentFinder.
        final SegmentFinder graphemeSegmentFinder =
                new GraphemeClusterSegmentFinder(text, layout.getPaint());
        // Create word SegmentFinder.
        final WordIterator wordIterator = getWordIterator();
        wordIterator.setCharSequence(text, 0, text.length());
        final SegmentFinder wordSegmentFinder = new WordSegmentFinder(text, wordIterator);
        // Create line SegmentFinder.
        final int lineCount = endLine - startLine + 1;
        final int[] lineRanges = new int[2 * lineCount];
        for (int line = startLine; line <= endLine; ++line) {
            final int offset = line - startLine;
            lineRanges[2 * offset] = layout.getLineStart(line);
            lineRanges[2 * offset + 1] = layout.getLineEnd(line);
        }
        final SegmentFinder lineSegmentFinder = new SegmentFinder.DefaultSegmentFinder(lineRanges);
        final TextBoundsInfo.Builder builder = new TextBoundsInfo.Builder();
        builder.setStartAndEnd(start, end)
                .setMatrix(localToGlobalMatrix)
                .setCharacterBounds(characterBounds)
                .setCharacterBidiLevel(characterBidiLevels)
                .setCharacterFlags(characterFlags)
                .setGraphemeSegmentFinder(graphemeSegmentFinder)
                .setLineSegmentFinder(lineSegmentFinder)
                .setWordSegmentFinder(wordSegmentFinder);
        return  builder.build();
    }
    /**
     * @hide
     */
+21 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT_START;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.RectF;
import android.os.Bundle;
import android.text.Editable;
import android.text.Selection;
@@ -46,9 +47,12 @@ import android.view.inputmethod.JoinOrSplitGesture;
import android.view.inputmethod.RemoveSpaceGesture;
import android.view.inputmethod.SelectGesture;
import android.view.inputmethod.SelectRangeGesture;
import android.view.inputmethod.TextBoundsInfo;
import android.view.inputmethod.TextBoundsInfoResult;
import android.widget.TextView;

import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;

/**
@@ -261,6 +265,23 @@ public final class EditableInputConnection extends BaseInputConnection
        return true;
    }

    @Override
    public void requestTextBoundsInfo(
            @NonNull RectF rectF, @Nullable @CallbackExecutor Executor executor,
            @NonNull Consumer<TextBoundsInfoResult> consumer) {
        final TextBoundsInfo textBoundsInfo = mTextView.getTextBoundsInfo(rectF);
        final int resultCode;
        if (textBoundsInfo != null) {
            resultCode = TextBoundsInfoResult.CODE_SUCCESS;
        } else {
            resultCode = TextBoundsInfoResult.CODE_FAILED;
        }
        final TextBoundsInfoResult textBoundsInfoResult =
                new TextBoundsInfoResult(resultCode, textBoundsInfo);

        executor.execute(() -> consumer.accept(textBoundsInfoResult));
    }

    @Override
    public boolean setImeConsumesInput(boolean imeConsumesInput) {
        if (mTextView == null) {