Android TextView 富文本之 ClickableSpan

前言

ClickableSpan可讓咱們在點擊TextView相應文字時響應點擊事件,好比經常使用的URLSpan,會在點擊時打開相應的連接。而爲了讓TextView可以響應ClickableSpan的點擊,咱們須要爲它設置LinkMovementMethod,可是這個LinkMovementMethod又有着很大的坑,接下來就總結下這些坑和個人解決辦法。java

LinkMovementMethod的坑

一、點不許

這裏將每一個字符都設置上ClickableSpan,並在點擊時Toast當前被點的字符(文字顏色和背景色應該是ClickableSpanLinkMovementMethod自動幫咱們設置的)。設置完LinkMovementMethod後,你會發現本身明明沒有點到相應的ClickableSpan,卻仍是響應了點擊事件,或者明明點到了卻不響應,還有的都點到文字外面了,仍是會有響應,以下圖。 git

二、ellipsize不起做用且TextView會滾

maxLines設置爲2ellipsizeend,卻發現不起做用,並且整個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找到了 解決方法,須要設置TextViewOnTouchListener,而後本身處理點擊事件,大體貼一下源碼。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;
    }
}
複製代碼

這段代碼基本上就是從LinkMovementMethodOnTouchListener拷貝過來的,咱們來看下效果。 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時的startend都是下標,這樣會使得獲得的ClickableSpan往左偏(注意,getOffsetForHorizontal是獲得的下標往右偏),這也就是使用LinkMovementMethod點不許的緣由,這裏要使end = index + 1
最後若是點擊到的字符是ClickableSpan,那就在ACTION_DOWN時直接返回true表示要處理該組觸摸事件,在ACTION_UP時響應ClickableSpan的點擊事件。orm

結束

至此,我遇到的ClickableSpan的坑和解決方法也都講清楚了,不少涉及源碼的地方也都沒有深刻研究,好比getOffsetForHorizontal獲得的下標爲何會往右偏之類的問題,以後還須要多多研究源碼,這樣才能提升本身。照例附上源碼 github.com/funnywolfda…
下一篇會總結下Html.formHtml超連接的處理,怎麼本身處理a標籤,拿到標籤屬性,同時響應點擊事件,在本地打開對應頁面。cdn

相關文章
相關標籤/搜索