ClickableSpan
可让咱们在点击TextView
相应文字时响应点击事件,好比经常使用的URLSpan
,会在点击时打开相应的连接。而为了让TextView
可以响应ClickableSpan
的点击,咱们须要为它设置LinkMovementMethod
,可是这个LinkMovementMethod
又有着很大的坑,接下来就总结下这些坑和个人解决办法。java
LinkMovementMethod
的坑这里将每一个字符都设置上ClickableSpan
,并在点击时Toast
当前被点的字符(文字颜色和背景色应该是ClickableSpan
和LinkMovementMethod
自动帮咱们设置的)。设置完LinkMovementMethod
后,你会发现本身明明没有点到相应的ClickableSpan
,却仍是响应了点击事件,或者明明点到了却不响应,还有的都点到文字外面了,仍是会有响应,以下图。 git
ellipsize
不起做用且TextView
会滚将maxLines
设置为2
,ellipsize
为end
,却发现不起做用,并且整个TextView
变成能够滚动的了。 github
咱们大体看下LinkMovementMethod
的实现。LinkMovementMethod
继承自ScrollingMovementMethod
,从名字能够看出来它是能够滚动的。他有一个onTouchEvent
方法,看来是处理点击事件的,它会在action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN
的时候去处理事件,得到点击位置的ClickableSpan
,在ACTION_UP
的时候响应点击事件。而在action == MotionEvent.ACTION_MOVE
的时候交给父类ScrollingMovementMethod
处理,这也就使TextView
能够滚动,整个TextView
能够滚动显示全部的文本,也就不会有ellipsize
的省略号了。
Android 这样处理LinkMovementMethod
多是为了在大量文字时更方便地阅读,能够上下滚动,点击的时候点击的位置能够不遮挡要点击文字。可是在有些状况下就不太适用了,好比只是想缩略的显示两行文本,而点击时要点那儿是那儿,这就须要咱们来本身处理TextView
的点击事件。ide
LinkMovementMethod
滚动的问题我当时在stackoverflow
找到了 解决方法,须要设置TextView
的OnTouchListener
,而后本身处理点击事件,大体贴一下源码。spa
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;
}
}
return false;
}
}
复制代码
这段代码基本上就是从LinkMovementMethod
的OnTouchListener
拷贝过来的,咱们来看下效果。 debug
TextView
再也不滚动,省略号也有了,很好的解决了
LinkMovementMethod
的问题,可是毕竟基本是拷贝过来的,原来点击
Span
不许的问题仍是存在。
Span
不许的问题LinkMovementMethod
在处理点击事件时没有作边缘判断,获得的点击位置结果可能不许,所以要本身手动处理这些边界的问题,通过反复实验,总算解决了这个问题,先来看下效果。 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;
}
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;
}
}
复制代码
首先在getTouchedIndex
中会首先获得点击的行line
,这里不能彻底相信layout.getLineForVertical
返回的数据,要本身判断下点击的位置是否真的在该行。而后经过layout.getOffsetForHorizontal
拿到对应的下标,这里要考虑两个问题,第一个是ellipsize
省略号的问题,经过layout.getEllipsisCount
拿到省略的字符数,在判断当前下标的字符是否是已经被省略了;第二个就是getOffsetForHorizontal
获得的下标会往右偏(就是点“和”
的右半边的时候会获得“谐”
的下标),这个你们能够本身打log
或者debug
试一下,判断下字符左边的横坐标大于 x
,就说明点的是前一个字符,要index -= 1
。
而后就是根据index
拿到对用的ClickableSpan
,经过Spanned.getSpans
就能拿获得,可是LinkMovementMethod
中调用getSpans
时的start
和end
都是下标,这样会使得获得的ClickableSpan
往左偏(注意,getOffsetForHorizontal
是获得的下标往右偏),这也就是使用LinkMovementMethod
点不许的缘由,这里要使end = index + 1
。
最后若是点击到的字符是ClickableSpan
,那就在ACTION_DOWN
时直接返回true
表示要处理该组触摸事件,在ACTION_UP
时响应ClickableSpan
的点击事件。orm
至此,我遇到的ClickableSpan
的坑和解决方法也都讲清楚了,不少涉及源码的地方也都没有深刻研究,好比getOffsetForHorizontal
获得的下标为何会往右偏之类的问题,以后还须要多多研究源码,这样才能提升本身。照例附上源码 github.com/funnywolfda…。
下一篇会总结下Html.formHtml
中超连接
的处理,怎么本身处理a
标签,拿到标签属性,同时响应点击事件,在本地打开对应页面。cdn