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?

Epaga
  • 38,231
  • 58
  • 157
  • 245

3 Answers3

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

Tony Blues
  • 879
  • 1
  • 9
  • 8
  • 1
    I asked the Google engineers at Google IO office hours, and this is the same thing they came up with. – James Moore May 10 '11 at 04:57
  • 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. :-) – einschnaehkeee Nov 29 '11 at 14:49
  • Thanks for saving my life:) – Mingfei Dec 22 '15 at 08:23
  • https://stackoverflow.com/questions/62528990/multiple-line-textview-getlineforvertical-returnin-wrong-line-number-in-android – ATES Jun 23 '20 at 07:00
  • in case someone wants the char position when knows the char index https://stackoverflow.com/a/56740416/2736039 – Ultimo_m May 18 '21 at 16:16
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.

snctln
  • 12,175
  • 6
  • 45
  • 42
  • 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. – Epaga Feb 22 '10 at 15:44
  • @Tony Blues answer has the pointToCharacterIndex sort of thing you're looking for. – James Moore Jul 06 '11 at 20:21
4

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.

devconsole
  • 7,875
  • 1
  • 34
  • 42
  • 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 :) – Jenix Jul 25 '18 at 19:52
  • 1
    @Jenix I just ran a quick test and it seems that [TextView.getOffsetForPosition](https://developer.android.com/reference/android/widget/TextView.html#getOffsetForPosition(float,%20float)) 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. – devconsole Jul 27 '18 at 09:07
  • 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? – Jenix Nov 24 '18 at 17:23
  • @Jenix I did test this again in an Android 9 emulator and I could not reproduce the issue. – devconsole Nov 28 '18 at 17:22
  • 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.. – Jenix Nov 29 '18 at 10:24
  • Ah I didn't test on Android 9 ones but there's no point if others don't work as expected. – Jenix Nov 29 '18 at 10:30
  • @Jenix Sorry, I was only testing with natural breaks (spaces in the input text), I guess it has nothing to do with the Android version. – devconsole Nov 29 '18 at 13:25
  • 1
    @Jenix Before I update my answer please test this version: https://gist.github.com/devconsole/64040ec74bde98f59f2d70aa6bfaf4e1 – devconsole Nov 29 '18 at 14:14
  • Great! Excellent! Works like a charm! Thanks a lot! – Jenix Nov 29 '18 at 15:05
  • Just curious. Do you think your code is more expensive than [TextView.getOffsetForPosition](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java)? – Jenix Nov 30 '18 at 08:47