Android——ClickableSpan of TextView rich text

foreword

ClickableSpanIt allows us TextViewto respond to click events when we click on the corresponding text, such as the commonly used ones URLSpan, which will open the corresponding link when clicked. In order to make a TextViewresponsive ClickableSpanclick, we need to set it up LinkMovementMethod, but this LinkMovementMethodhas a lot of pitfalls. Next, I will summarize these pitfalls and my solution.

The pit of LinkMovementMethod

1. Inaccurate

Here, each character is set ClickableSpan, and Toastthe character that is currently clicked when clicked (the text color and background color should be ClickableSpanautomatically LinkMovementMethodset for us). LinkMovementMethodAfter setting , you will find that you obviously did ClickableSpannot click on the corresponding one, but still responded to the click event, or you clicked on it but did not respond, and some clicked outside the text, but there will still be a response, as shown in the figure below.
insert image description here

2. Ellipsize does not work and TextView will scroll

Set maxLinesit to 2, ellipsizebut endfound that it didn't work, and the whole TextViewthing became scrollable.
insert image description here
Simple analysis

Let's take a closer look LinkMovementMethodat the implementation. LinkMovementMethodInherited from ScrollingMovementMethod, as can be seen from the name, it is scrollable. He has a onTouchEventmethod that seems to handle the click event. It will action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWNhandle the event when it is, get the click position ClickableSpan, and ACTION_UPrespond to the click event when it is. And in action == MotionEvent.ACTION_MOVE的时候交给父类ScrollingMovementMethodprocessing, this also makes it TextViewpossible to scroll, and the entire TextViewtext can be scrolled to display, and there will be ellipsizeno ellipsis.

Android may do this LinkMovementMethodto make it easier to read when there are a lot of text, you can scroll up and down, and when you click, the click position can not block the text to be clicked. But in some cases, it is not suitable, for example, just want to display two lines of text in abbreviated form, but when you click, the key point is there, which requires us to handle the click event by ourselves TextView.

Solve the problem of LinkMovementMethod scrolling

stackoverflowI found a solution at the time , I needed to set it TextViewup OnTouchListener, and then I handled the click event myself, and roughly posted the source code.

public static class ClickableSpanTouchListener implements View.OnTouchListener {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (!(v instanceof TextView)) {
            return false;
        }
        TextView widget = (TextView) v;
        CharSequence text = widget.getText();
        if (!(text instanceof 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;//在这里返回true,textView.setHighlightColor才有效
                }
            }
        }
        return false;
    }
}

This code is basically copied from LinkMovementMethod, OnTouchListenerlet's see the effect.
insert image description here
TextViewNo more scrolling, and the ellipsis is also there, which solves the problem very well LinkMovementMethod, but after all, it is basically copied, and the original 点击Span不准problem still exists.

Solve the problem that clicking Span is not allowed

LinkMovementMethodWhen processing the click event, no edge judgment is made, and the result of the click position may be inaccurate. Therefore, it is necessary to manually deal with these boundary problems. After repeated experiments, this problem is finally solved. Let’s take a look at the effect first.
insert image description here
The source code is as follows:

public static class ClickableSpanTouchListener implements View.OnTouchListener {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (!(v instanceof TextView)) {
            return false;
        }
        TextView widget = (TextView) v;
        CharSequence text = widget.getText();
        if (!(text instanceof 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 (!(charSequence instanceof Spanned)) {
            return null;
        }
        Spanned buffer = (Spanned) charSequence;
        // end 应该是 index + 1,如果也是 index,得到的结果会往左偏
        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();
        // 根据 y 得到对应的行 line
        int line = layout.getLineForVertical(y);
        // 判断得到的 line 是否正确
        if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)
                || y < layout.getLineTop(line) || y > layout.getLineBottom(line)) {
            return -1;
        }
        // 根据 line 和 x 得到对应的下标
        int index = layout.getOffsetForHorizontal(line, x);
        // 这里考虑省略号的问题,得到真实显示的字符串的长度,超过就返回 -1
        int showedCount = widget.getText().length() - layout.getEllipsisCount(line);
        if (index > showedCount) {
            return -1;
        }
        // getOffsetForHorizontal 获得的下标会往右偏
        // 获得下标处字符左边的左边,如果大于点击的 x,就可能点的是前一个字符
        if (layout.getPrimaryHorizontal(index) > x) {
            index -= 1;
        }
        return index;
    }
}

First of all, getTouchedIndexyou will first get the clicked line in , here you can't completely trust layout.getLineForVerticalthe returned data, you have to judge whether the clicked position is really in the line. Then by layout.getOffsetForHorizontalgetting the corresponding subscript, there are two issues to consider here. The first is ellipsizethe ellipsis problem. By layout.getEllipsisCountgetting the number of omitted characters, it is judged whether the character of the current subscript has been omitted; the second is to getOffsetForHorizontalget The subscript will be biased to the right (that is, when you click on the right half of "和", you will get the subscript of "harmony"). You can type this or try it yourself. If you judge that the abscissa on the left side of the character is greater than that, it logmeans debugthe xpoint is the previous character, to be index -= 1.

Then it is based on indexgetting the right one ClickableSpan, Spanned.getSpansand you can get it by passing it, but the sum of the call LinkMovementMethodin the middle is all subscripts, which will make the obtained ones go to the left (note that the obtained subscripts go to the right), which is The reasons for the inaccurate use of points are explained here .getSpansstartendClickableSpangetOffsetForHorizontalLinkMovementMethodend = index + 1

Finally, if the clicked character is ClickableSpan, then ACTION_DOWNdirectly return the click event truethat indicates that the group of touch events is to be processed and ACTION_UPresponded to at the time ClickableSpan.

Finish

So far, the pitfalls and solutions I have encountered ClickableSpanhave been explained clearly, and many places related to the source code have not been studied in depth, such as why the getOffsetForHorizontalobtained subscript is biased to the right, etc. After that, I need to study the source code more. Only in this way can we improve ourselves. As usual, attach the source code github.com/funnywolfda… .

Original link: https://juejin.im/post/5c84902ce51d453ce668b750

Guess you like

Origin blog.csdn.net/u012230055/article/details/103148177