作者:朱玉龙1977 | 来源:互联网 | 2023-09-08 17:18
前言ClickableSpan可以让我们在点击TextView相应文字时响应点击事件,比方常用的URLSpan,会在点击时打开相应的链接。而为了让TextView能够响应Click
前言
ClickableSpan
可以让我们在点击TextView
相应文字时响应点击事件,比方常用的URLSpan
,会在点击时打开相应的链接。而为了让TextView
能够响应ClickableSpan
的点击,我们需要为它设置LinkMovementMethod
,但是这个LinkMovementMethod
又有着很大的坑,接下来就总结下这些坑和我的处理办法。
LinkMovementMethod
的坑
1、点不准
这里将每个字符都设置上ClickableSpan
,并在点击时Toast
当前被点的字符(文字颜色和背景色应该是ClickableSpan
和LinkMovementMethod
自动帮我们设置的)。设置完LinkMovementMethod
后,你会发现自己明明没有点到相应的ClickableSpan
,却还是响应了点击事件,或者者明明点到了却不响应,还有的都点到文字外面了,还是会有响应,如下图。
image
2、ellipsize
不起作用且TextView
会滚
将maxLines
设置为2
,ellipsize
为end
,却发现不起作用,而且整个TextView
变成可以滚动的了。
image
简单分析下
我们大致看下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
的点击事件。
处理LinkMovementMethod
滚动的问题
我当时在stackoverflow
找到了
处理方法,需要设置TextView
的OnTouchListener
,而后自己解决点击事件,大致贴一下源码。
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
拷贝过来的,我们来看下效果。
image
TextView
不再滚动,省略号也有了,很好的处理了
LinkMovementMethod
的问题,但是毕竟基本是拷贝过来的,原来点击
Span
不准的问题还是存在。
处理点击Span
不准的问题
LinkMovementMethod
在解决点击事件时没有做边缘判断,得到的点击位置结果可能不准,因而要自己手动解决这些边界的问题,经过反复试验,总算处理了这个问题,先来看下效果。
image
源码如下:
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.getLineRight(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
的点击事件。
结束
至此,我遇到的ClickableSpan
的坑和处理方法也都讲清楚了,很多涉及源码的地方也都没有深入研究,比方getOffsetForHorizontal
得到的下标为什么会往右偏之类的问题,之后还需要多多研究源码,这样才能提高自己。照例附上源码 funnywolfdadada/RichTextDemo。
下一篇会总结下Html.formHtml
中超链接
的解决,怎样自己解决a
标签,拿到标签属性,同时响应点击事件,在本地打开对应页面。