LinkMovementMethod与URLSpan相关的两个问题

通过LinkMovementMethod与URLSpan给TextView添加链接就不说了,大家都知道,不过在使用过程中遇到两个问题,在这里记录一下。

  • 一是LinkMovementMethod导致TextView可滚动,可能使文本错位。
  • 二是行尾链接的点击区域,可能点击行尾的空白仍会触发链接点击,有些机型甚至会同时触发View.onClickListener.

LinkMovementMethod导致TextView可滚动

有时要做和微信朋友圈类似的一个功能,当文本很长时只显示几行,然后有一个按钮可以使文本显示全文或收起。

这个的实现方式一般都是setMaxLine或都根据行高修改LayoutParams的高度。这个本来没有什么。但是如果文本中包含链接,我们就需要添加URLSpan,而如果要URLSpan起作用还需要给TextView设置LinkMovementMethod。这就引起了这里说的问题。

由于设置了maxLine或LayoutParams导致TextView其实是没有显示所有内容的,实际高度小于展示所有内容需要的高度,同时由于设置了LinkMovementMethod,LinkMovementMethod继承自ScrollingMovementMethod,这时其实这个TextView就是可滚动的,不信的话可以开个模拟器设置maxLine试试,鼠标放到TextView上滚动滚轮,你就会看到效果了。

如果这个TextView在ListView或ScrollView中,当滑动ListView时ListView会拦截事件进行滑动,一般也不会出问题,但很偶然的情况下,TextView会先滚动一点点距离,然后ListView才拦截了事件进行滑动(手指按住先横向滑动再竖着滑动容易出现),结果就是TextView中的文本显示错位了。

TextView接收到TouchEvent事件时会把它传递给MovementMethod处理,所以要从LinkMovementMethod下手了,写一个类继承自LinkMovementMethod,重写onTouch方法,添加以下代码就可以了,只是简单地拦截ACTION_MOVE

1
2
3
4
5
6
7
8
9
10
@Override
public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer,
@NonNull MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_MOVE) {
// Prevent scroll event
return true;
}
return super.onTouchEvent(widget, buffer, event);
}

链接在TextView的行尾,点击行尾的空白处仍会触发链接的点击跳转

这个同样是重写LinkMovementMethod.onTouch,跟Span点击相关的基本都在MovementMethod的onTouch中,我们的目的是点击行尾的空白时不触发URLSpan的点击跳转(因为点击空白处其实是没有点击到Span上的),想取消点击事件的触发,跟进源码看看它是怎么触发的,破坏掉它的条件就行了,这里就直接贴出来改好后的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Override
public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer,
@NonNull MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_MOVE) {
// Prevent scroll event
return true;
}
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[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
ClickableSpan cs = link[0];
int csStart = buffer.getSpanStart(cs);
int csEnd = buffer.getSpanEnd(cs);
// offset在Span的区间内才认为是点击到了Span上。默认的处理是,如果点击在行尾,即使点击在了
// Span外也认为点击了Span,判断点击offset小于line end的原因是点击内容区外面系统也认为点
// 击的位置是最后一个文字,影响就是如果链接在行尾,点最后一个字母没反应,不过影响不大
if (off >= csStart && off < csEnd && off < layout.getLineEnd(line)) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else {
Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]));
}
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}

代码中也加了点注释,所做的操作就是,通过layout.getOffsetForHorizontal(line, x)获取用户点击了一行中的哪个字符,获取点击位置包含的ClickableSpan,然后判断offset在span的start与end区间内才算点击了Span,但当点到行尾的空白处时系统也认为点击了最后一个字符,所以如果链接正好在行尾就会认为点击了链接,所以同时判断了如果点击的是行属的最后一个字符就不触发链接点击事件。由于只有当链接在行尾时才有影响,并且手指点上去基本同时点到几个字符,所以影响不大。