foreword
ClickableSpan
It allows us TextView
to 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 TextView
responsive ClickableSpan
click, we need to set it up LinkMovementMethod
, but this LinkMovementMethod
has 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 Toast
the character that is currently clicked when clicked (the text color and background color should be ClickableSpan
automatically LinkMovementMethod
set for us). LinkMovementMethod
After setting , you will find that you obviously did ClickableSpan
not 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.
2. Ellipsize does not work and TextView will scroll
Set maxLines
it to 2, ellipsize
but end
found that it didn't work, and the whole TextView
thing became scrollable.
Simple analysis
Let's take a closer look LinkMovementMethod
at the implementation. LinkMovementMethod
Inherited from ScrollingMovementMethod
, as can be seen from the name, it is scrollable. He has a onTouchEvent
method that seems to handle the click event. It will action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN
handle the event when it is, get the click position ClickableSpan
, and ACTION_UP
respond to the click event when it is. And in action == MotionEvent.ACTION_MOVE的时候交给父类ScrollingMovementMethod
processing, this also makes it TextView
possible to scroll, and the entire TextView
text can be scrolled to display, and there will be ellipsize
no ellipsis.
Android may do this LinkMovementMethod
to 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
stackoverflow
I found a solution at the time , I needed to set it TextView
up 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
, OnTouchListener
let's see the effect.
TextView
No 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
LinkMovementMethod
When 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.
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, getTouchedIndex
you will first get the clicked line in , here you can't completely trust layout.getLineForVertical
the returned data, you have to judge whether the clicked position is really in the line. Then by layout.getOffsetForHorizontal
getting the corresponding subscript, there are two issues to consider here. The first is ellipsize
the ellipsis problem. By layout.getEllipsisCount
getting the number of omitted characters, it is judged whether the character of the current subscript has been omitted; the second is to getOffsetForHorizontal
get 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 log
means debug
the x
point is the previous character, to be index -= 1
.
Then it is based on index
getting the right one ClickableSpan
, Spanned.getSpans
and you can get it by passing it, but the sum of the call LinkMovementMethod
in 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 .getSpans
start
end
ClickableSpan
getOffsetForHorizontal
LinkMovementMethod
end = index + 1
Finally, if the clicked character is ClickableSpan
, then ACTION_DOWN
directly return the click event true
that indicates that the group of touch events is to be processed and ACTION_UP
responded to at the time ClickableSpan
.
Finish
So far, the pitfalls and solutions I have encountered ClickableSpan
have been explained clearly, and many places related to the source code have not been studied in depth, such as why the getOffsetForHorizontal
obtained 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