preface

ClickableSpan allows us to respond to click events when we click on the corresponding text of a TextView, such as the commonly used URLSpan, which opens the corresponding link when clicked. In order for the TextView to respond to ClickableSpan clicks, we need to set the LinkMovementMethod. However, the LinkMovementMethod has a large pit.

LinkMovementMethodThe pit of

1. The dot is not correct

Here we set ClickableSpan to each character and Toast the current character when clicked (the text color and background color should be set automatically for us by ClickableSpan and LinkMovementMethod). After setting the LinkMovementMethod, you may find yourself responding to ClickableSpan events even though the corresponding ClickableSpan is not clicked, or responding even though the corresponding ClickableSpan is clicked outside the text, as shown in the figure below.

2,ellipsizeDoes not work andTextViewWill roll

Setting maxLines to 2 and Ellipsize to end does not work and the entire TextView becomes scrollable.

Just a quick analysis

Let’s take a look at the implementation of LinkMovementMethod. LinkMovementMethod inherits from ScrollingMovementMethod, which, as its name suggests, is scrollable. He has a onTouchEvent method, seems to be handling the click event, it will be in action = = MotionEvent. ACTION_UP | | action = = MotionEvent. ACTION_DOWN to handle events, The ClickableSpan gets the click position and responds to the click event in ACTION_UP. When action == motionEvent.action_move is handled by the parent class ScrollingMovementMethod, this allows the TextView to scroll. The entire TextView can scroll all the text. There would be no ellipsis of Ellipsize. Android handles LinkMovementMethod this way probably to make it easier to read when there is a lot of text. You can scroll up and down, and click on it in a position that does not block the text to be clicked. But there are situations where it’s not very useful, such as when you want to display two lines of text in a thumbnail format, but when you click on it, the bullet points are there and there, and you need to handle the TextView click event yourself.

To solveLinkMovementMethodThe problem of scrolling

The solution I found in StackOverflow was to set the OnTouchListener of the TextView and then handle the click event myself, Posting the source code roughly.

public static class ClickableSpanTouchListener implements View.OnTouchListener {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(! (vinstanceof TextView)) {
            return false;
        }
        TextView widget = (TextView) v;
        CharSequence text = widget.getText();
        if(! (textinstanceof Spanned)) {
            return false;
        }
        Spanned buffer = (Spanned) text;
        int action = event.getAction();
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();

            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();

            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);

            if(links.length ! =0) {
                ClickableSpan link = links[0];
                if (action == MotionEvent.ACTION_UP) {
                    link.onClick(widget);
                }
                return true; }}return false; }}Copy the code

This code is basically copied from LinkMovementMethod’s OnTouchListener, so let’s see what it looks like.

TextView
LinkMovementMethod
Span

To solve the clickSpanWrong question

LinkMovementMethod does not make edge judgment when processing click events, and the result of click position may be inaccurate. Therefore, we have to handle these boundary problems manually by ourselves. After repeated experiments, we finally solved this problem, and let’s take a look at the effect first.

public static class ClickableSpanTouchListener implements View.OnTouchListener {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(! (vinstanceof TextView)) {
            return false;
        }
        TextView widget = (TextView) v;
        CharSequence text = widget.getText();
        if(! (textinstanceof Spanned)) {
            return false;
        }
        int action = event.getAction();
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            int index = getTouchedIndex(widget, event);
            ClickableSpan link = getClickableSpanByIndex(widget, index);
            if(link ! =null) {
                if (action == MotionEvent.ACTION_UP) {
                    link.onClick(widget);
                }
                return true; }}return false;
    }

    public static ClickableSpan getClickableSpanByIndex(TextView widget, int index) {
        if (widget == null || index < 0) {
            return null;
        }
        CharSequence charSequence = widget.getText();
        if(! (charSequenceinstanceof Spanned)) {
            return null;
        }
        Spanned buffer = (Spanned) charSequence;
        // End should be index + 1. If it is also index, the result will be skewed to the left
        ClickableSpan[] links = buffer.getSpans(index, index + 1, ClickableSpan.class);
        if(links ! =null && links.length > 0) {
            return links[0];
        }
        return null;
    }

    public static int getTouchedIndex(TextView widget, MotionEvent event) {
        if (widget == null || event == null) {
            return -1;
        }
        int x = (int) event.getX();
        int y = (int) event.getY();

        x -= widget.getTotalPaddingLeft();
        y -= widget.getTotalPaddingTop();

        x += widget.getScrollX();
        y += widget.getScrollY();

        Layout layout = widget.getLayout();
        // Get the corresponding line according to y
        int line = layout.getLineForVertical(y);
        // Check if the line is correct
        if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)
                || y < layout.getLineTop(line) || y > layout.getLineBottom(line)) {
            return -1;
        }
        // Get the corresponding subscripts according to line and x
        int index = layout.getOffsetForHorizontal(line, x);
        // The ellipsis problem is considered here, to get the actual length of the string, return -1 if it exceeds
        int showedCount = widget.getText().length() - layout.getEllipsisCount(line);
        if (index > showedCount) {
            return -1;
        }
        // getOffsetForHorizontal gets a horizontal index to the right
        // Get the left of the character to the left of the subscript. If it is greater than x, it may have clicked on the previous character
        if (layout.getPrimaryHorizontal(index) > x) {
            index -= 1;
        }
        returnindex; }}Copy the code

First in getTouchedIndex will get click on the first line of the line, here can’t fully believe layout. GetLineForVertical returned data, want to determine whether the click on the location of the real in the bank. Then through layout. GetOffsetForHorizontal get corresponding subscript, here want to consider two questions, the first is the issue of ellipsize ellipsis, through layout. GetEllipsisCount to omit the number of characters, Determine if the character of the current subscript has been omitted; The second is that the horizontal index of getOffsetForHorizontal will move to the right. You can log or debug this to check if the horizontal value of the character is greater than x. If the horizontal value of getOffsetForHorizontal is higher than x, it will move to the right. Index -= 1. You can then get ClickableSpan spans by index, which you get by Spanned. GetSpans, but you getSpans of start and end when you call getSpans in the LinkMovementMethod, This causes the resulting ClickableSpan to drift left (note that getOffsetForHorizontal is the resulting index to drift right), which is why the LinkMovementMethod is wrong, with end = index + 1. Finally, if the ClickableSpan character is clicked, return true on ACTION_DOWN to indicate that the set of touch events are being processed, and respond to ClickableSpan click events on ACTION_UP.

The end of the

So far, the ClickableSpan pothole and solution I encountered have been explained, and many of the source code issues have not been studied in depth, such as why the index of getOffsetForHorizontal goes to the right, and so on. I need to study the source code more to improve myself. As usual attached source github.com/funnywolfda… . The next article will summarize the handling of hyperlinks in html. formHtml, how to handle the A tag, get the tag attributes, and respond to the click event to open the corresponding page locally.