Android——TextView 富文本之 ClickableSpan

前言

ClickableSpan可以讓我們在點擊TextView相應文字時響應點擊事件,比如常用的URLSpan,會在點擊時打開相應的鏈接。而爲了讓TextView能夠響應ClickableSpan的點擊,我們需要爲它設置LinkMovementMethod,但是這個LinkMovementMethod又有着很大的坑,接下來就總結下這些坑和我的解決辦法。

LinkMovementMethod的坑

1、點不準

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

2、ellipsize不起作用且TextView會滾

maxLines設置爲2,ellipsizeend,卻發現不起作用,而且整個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找到了解決方法,需要設置TextViewOnTouchListener,然後自己處理點擊事件,大致貼一下源碼。

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;
    }
}

這段代碼基本上就是從LinkMovementMethodOnTouchListener拷貝過來的,我們來看下效果。
在這裏插入圖片描述
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時的startend都是下標,這樣會使得得到的ClickableSpan往左偏(注意,getOffsetForHorizontal是得到的下標往右偏),這也就是使用LinkMovementMethod點不準的原因,這裏要使end = index + 1

最後如果點擊到的字符是ClickableSpan,那就在ACTION_DOWN時直接返回true表示要處理該組觸摸事件,在ACTION_UP時響應ClickableSpan的點擊事件。

結束

至此,我遇到的ClickableSpan的坑和解決方法也都講清楚了,很多涉及源碼的地方也都沒有深入研究,比如getOffsetForHorizontal得到的下標爲什麼會往右偏之類的問題,之後還需要多多研究源碼,這樣才能提高自己。照例附上源碼 github.com/funnywolfda…

原文鏈接:https://juejin.im/post/5c84902ce51d453ce668b750

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章