Android: How to determine character index of a touch event's position in TextView?

I have a TextView with an OnTouchListener. What I want is the character index the user is pointing to when I get the MotionEvent. Is there any way to get to the underlying font metrics of the TextView?

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

Have you tried something like this:

Layout layout = this.getLayout();
if (layout != null)
{
    int line = layout.getLineForVertical(y);
    int offset = layout.getOffsetForHorizontal(line, x);

    // At this point, "offset" should be what you want - the character index
}

Hope this helps…

Solution 2

I am not aware of a simple direct way to do this but you should be able to put something together using the Paint object of the TextView via a call to TextView.getPaint()

Once you have the paint object you will have access to the underlying FontMetrices via a call to Paint.getFontMetrics() and have access to other functions like Paint.measureText() Paint.getTextBounds(), and Paint.getTextWidths() for accessing the actual size of the displayed text.

Solution 3

While it generally works I had a few problems with the answer from Tony Blues.

Firstly getOffsetForHorizontal returns an offset even if the x coordinate is way beyond the last character of the line.

Secondly the returned character offset sometimes belongs to the next character, not the character directly underneath the pointer. Apparently the method returns the offset of the nearest cursor position. This may be to the left or to the right of the character depending on what’s closer by.

My solution uses getPrimaryHorizontal instead to determine the cursor position of a certain offset and uses binary search to find the offset underneath the pointer’s x coordinate.

public static int getCharacterOffset(TextView textView, int x, int y) {
    x += textView.getScrollX() - textView.getTotalPaddingLeft();
    y += textView.getScrollY() - textView.getTotalPaddingTop();

    final Layout layout = textView.getLayout();

    final int lineCount = layout.getLineCount();
    if (lineCount == 0 || y < layout.getLineTop(0) || y >= layout.getLineBottom(lineCount - 1))
        return -1;

    final int line = layout.getLineForVertical(y);
    if (x < layout.getLineLeft(line) || x >= layout.getLineRight(line))
        return -1;

    int start = layout.getLineStart(line);
    int end = layout.getLineEnd(line);

    while (end > start + 1) {
        int middle = start + (end - start) / 2;

        if (x >= layout.getPrimaryHorizontal(middle)) {
            start = middle;
        }
        else {
            end = middle;
        }
    }

    return start;
}

Edit: This updated version works better with unnatural line breaks, when a long word does not fit in a line and gets split somewhere in the middle.

Caveats: In hyphenated texts, clicking on the hyphen at the end of a line return the index of the character next to it. Also this method does not work well with RTL texts.

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply