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

3

11

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?

Cesaro answered 20/2, 2010 at 16:44 Comment(0)
T
5

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.

Tajuanatak answered 20/2, 2010 at 17:4 Comment(2)
Thanks, I ended up using this. Too bad there's no simple "pointToCharacterIndex" type of method built-in, but it wasn't rocket science to roll my own thanks to Paint.Cesaro
@Tony Blues answer has the pointToCharacterIndex sort of thing you're looking for.Anyhow
N
26

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...

Niche answered 28/4, 2011 at 10:13 Comment(4)
I asked the Google engineers at Google IO office hours, and this is the same thing they came up with.Anyhow
I'm so happy I found this answer, thanks Tony! It works perfectly fine! Now I don't have any problems dealing with touch events even if the TextView contains images. :-)Farra
#62529490Richmond
in case someone wants the char position when knows the char index https://mcmap.net/q/672782/-find-exact-coordinates-of-a-single-character-inside-a-textviewUnstring
T
5

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.

Tajuanatak answered 20/2, 2010 at 17:4 Comment(2)
Thanks, I ended up using this. Too bad there's no simple "pointToCharacterIndex" type of method built-in, but it wasn't rocket science to roll my own thanks to Paint.Cesaro
@Tony Blues answer has the pointToCharacterIndex sort of thing you're looking for.Anyhow
T
5

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.

Test answered 6/4, 2018 at 12:5 Comment(10)
Thanks for the info. But my question is, TextView's getOffsetForPosition() didn't work for you? It also uses getLineForVertical() and getOffsetForHorizontal() under the hood but it does some extra calculation to be more precise. I voted anyway :)Blayze
@Blayze I just ran a quick test and it seems that TextView.getOffsetForPosition also often returns the offset of the next character to the right instead of the character directly underneath the pointer. Try to increase the font size of the text view to something quite large to see for yourself.Test
Hey, it's me again! :) While I like your way the most, this has one serious (at least to me) problem. Whenever the automatic line-break works on the text, this code fails to pick up every last character in every line except for the very last one. If I put line-break characters manually at the same positions in the text, it works just fine. Do you know why this happens?Blayze
@Blayze I did test this again in an Android 9 emulator and I could not reproduce the issue.Test
<TextView android:id="@+id/textView" android:textSize="30sp" android:layout_width="100dp" android:layout_height="wrap_content" android:text="abcdefghijklmnopqrstuvwxyz" />Blayze
I created a new empty android proejct and put a TextView which is the same as above. Using your code, I get -1 when I touch the characters f, m, s, y which are the last ones in each lines. I did many tests like this, and did again after reading your new comment but the same. Confirmed on Samsung Galaxy Note 2, S7 Edge, and even on Genymotion emulators..Blayze
Ah I didn't test on Android 9 ones but there's no point if others don't work as expected.Blayze
@Blayze Sorry, I was only testing with natural breaks (spaces in the input text), I guess it has nothing to do with the Android version.Test
@Blayze Before I update my answer please test this version: gist.github.com/devconsole/64040ec74bde98f59f2d70aa6bfaf4e1Test
Just curious. Do you think your code is more expensive than TextView.getOffsetForPosition?Blayze

© 2022 - 2024 — McMap. All rights reserved.