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