前言
ClickableSpan
可以讓我們在點擊TextView
相應文字時響應點擊事件,比如常用的URLSpan
,會在點擊時打開相應的鏈接。而爲了讓TextView
能夠響應ClickableSpan
的點擊,我們需要爲它設置LinkMovementMethod
,但是這個LinkMovementMethod
又有着很大的坑,接下來就總結下這些坑和我的解決辦法。
LinkMovementMethod的坑
1、點不準
這裏將每個字符都設置上ClickableSpan
,並在點擊時Toast
當前被點的字符(文字顏色和背景色應該是ClickableSpan
和LinkMovementMethod
自動幫我們設置的)。設置完LinkMovementMethod
後,你會發現自己明明沒有點到相應的ClickableSpan
,卻還是響應了點擊事件,或者明明點到了卻不響應,還有的都點到文字外面了,還是會有響應,如下圖。
2、ellipsize不起作用且TextView會滾
將maxLines
設置爲2,ellipsize
爲end
,卻發現不起作用,而且整個TextView
變成可以滾動的了。
簡單分析下
我們大致看下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;//在這裏返回true,textView.setHighlightColor纔有效
}
}
}
return false;
}
}
這段代碼基本上就是從LinkMovementMethod
的OnTouchListener
拷貝過來的,我們來看下效果。
TextView
不再滾動,省略號也有了,很好的解決了LinkMovementMethod
的問題,但是畢竟基本是拷貝過來的,原來點擊Span不準
的問題還是存在。
解決點擊Span不準的問題
LinkMovementMethod
在處理點擊事件時沒有做邊緣判斷,得到的點擊位置結果可能不準,因此要自己手動處理這些邊界的問題,經過反覆實驗,總算解決了這個問題,先來看下效果。
源碼如下:
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
的點擊事件。
結束
至此,我遇到的ClickableSpan
的坑和解決方法也都講清楚了,很多涉及源碼的地方也都沒有深入研究,比如getOffsetForHorizontal
得到的下標爲什麼會往右偏之類的問題,之後還需要多多研究源碼,這樣才能提高自己。照例附上源碼 github.com/funnywolfda…。
原文鏈接:https://juejin.im/post/5c84902ce51d453ce668b750