Android的TextView除了可以用來顯示文字資料之外,還可以使用HTML語法來調整文字的樣式和文字超連結,無需特地去使用WebView。只是一旦使用TextView製作超連結,該TextView就會變得難以控制,因此有幾個特性應該是設計師必須事先知道的。



TextView顯示HTML

要使用TextView顯示HTML的作法很簡單,只要用以下方式來setText即可。

textView.setText(Html.fromHtml("HTML語法字串"));

例如:

textView.setText(Html.fromHtml("歡迎大家來到<big><font color="#FF0000"><b>MagicLen</b></font></big>,本站網址如下:<br/><br/><a href="https://magiclen.org/">https://magiclen.org/</a>"));

結果如下:

啟用HTML超連結

如果只是單單使用TextView的setText方法來指定HTML語法,HTML超連結將無法有真正的作用,可以使用Android SDK內建提供的方式來讓TextView擁有超連結的功能,程式只要添加一行即可,如下:

textView.setMovementMethod(LinkMovementMethod.getInstance());

指定LinkMovementMethod給textView,使其擁有超連結的功能。

TextView使用AutoLink自動判斷連結

若資料來源並非是HTML,而是純文字的話,如果想要讓文字中的網址、電子郵件等等的資訊也擁有超連結,可以設定TextView的AutoLink屬性,決定要將哪些資訊自動產生超連結。

在XML檔案中設定autolink屬性

添加方式如下:

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:autoLink="all" />

若將android:autoLink設為all,那它會將text文字中的網址、電子郵件、地址、電話都變成超連結。如果沒有特殊需求的話,不要自動將電話變成超連結,因為不是所有的Android裝置都能夠打電話,且電話格式各國都不太一樣,很大的機率會將一般數字誤判成電話。也不要自動將地址變成超連結,因為並非在所有裝置上都有地圖App,而且Android好像沒辦法判斷中文地址。所以autoLink屬性可以改寫如下:

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:autoLink="web|email" />

如果在XML設定autoLink,啟用了AutoLink屬性的話,LinkMovementMethod將會被自動加入TextView中,因此沒有必要使用setMovementMethod再指定LinkMovementMethod。

在SDK中用Java程式指定AutoLinkMask

添加方式如下:

textView.setAutoLinkMask(Linkify.ALL);

不將電話和地址加上超連結的寫法如下:

textView.setAutoLinkMask(Linkify.EMAIL_ADDRESSES|Linkify.WEB_URLS);

僅用setAutoLinkMask方法是無法啟用AutoLink屬性的,還再指定LinkMovementMethod給TextView:

textView.setMovementMethod(LinkMovementMethod.getInstance());

不必使用HTML的a標籤指定超連結

啟用textView的AutoLink之後,就不必再使用HTML的a標籤來製作超連結。因為Android會自動抓取text文字中符合的格式,將其自動更換為HTML的超連結。當然如果想要讓超連結顯示出不同的文字,還是可以使用a標籤來指定。完全不使用HTML,Android也會自動產生超連結。

以下舉個例子:

textView.setText("歡迎大家來到MagicLenn本站網址:https://magiclen.orgn電子郵件:len@magiclen.org");
textView.setAutoLinkMask(Linkify.EMAIL_ADDRESSES|Linkify.WEB_URLS);
textView.setMovementMethod(LinkMovementMethod.getInstance());

結果如下:

支援超連結的TextView所產生的問題

要讓TextView擁有超連結功能,必須要指定LinkMovementMethod給TextView。然而TextView一旦設定了MovementMethod或是KeyListener,TextView會自動將Focusable、Clickable、LongClickable屬性設定為true。

問題與解決方法

當TextView為Focusable的時候,且此TextView作為GridView或是ListView的Item,會使得無論是否點擊到TextView,onItemClickListener和onItemLongClickListener都會沒有作用。所以一定要覆寫掉hasFocusable方法,讓他永遠傳回false。

當TextView為Clickable的時候,onTouchEvent將永遠回傳true,所以點擊事件在TextView中就被擋下了,無法傳給更上層的View,因此當此TextView作為GridView或是ListView的Item時,onItemClickListener和onItemLongClickListener將無作用,所以一定要想辦法覆寫掉onTouchEvent方法,讓它可以在點擊到TextView中連結的時候傳回true就好。

但是,Android內建的LinkMovementMethod並不會讓我們知道使用者到底是不是點擊連結,因此我們還必須改寫LinkMovementMethod。

改寫後的TextView命名為LinkTextView,LinkMovementMethod命名為LinkTextViewMovementMethod,程式碼如下:

/**
 * 解決可使用Link HTML的TextView變成Clickable和Focusable的問題
 *
 * @author magiclen
 */
public class LinkTextView extends TextView {

    // -----物件變數-----
    private boolean linkHit; // 是否為按下連結

    // -----建構子-----
    public LinkTextView(Context context) {
        super(context);
    }

    public LinkTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public LinkTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    // -----物件方法-----
    /**
     * 覆寫onTouchEvent,使其不會永遠傳回true,若為true,則無法將touch事件傳出給上層的View。
     *
     */
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        return linkHit;
    }

    /**
     * 設定HTML內容給TextView,如果已啟用AutoLink屬性,則不需要使用這個方法來製作a標籤的連結。
     *
     * @param html
     */
    public void setTextViewHTML(String html) {
        Spanned sequence = Html.fromHtml(html);
        SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
        setText(strBuilder);
    }

    /**
     * 設定MovementMethod之後,TextView會成為Focusable,所以LinkTextView覆寫此方法,永遠傳回false。
     */
    @Override
    public boolean hasFocusable() {
        return false;
    }

    /**
     * 繼承LinkMovementMethod的LinkTextViewMovementMethod,將會針對連結點擊進行處理,
     * 讓LinkTextView知道目前點擊是否為連結點擊。
     *
     * @author magiclen
     *
     */
    public static class LinkTextViewMovementMethod extends LinkMovementMethod {

        // -----類別變數-----
        private static LinkTextViewMovementMethod sInstance; // 儲存唯一的實體參考

        // -----物件方法-----
        /**
         * 取得LinkTextViewMovementMethod的唯一實體參考。
         *
         * @return
         */
        public static LinkTextViewMovementMethod getInstance() {
            if (sInstance == null) {
                sInstance = new LinkTextViewMovementMethod(); // 建立新的實體
            }
            return sInstance;
        }

        /**
         * 覆寫觸控事件,分辨出是否為連結的點擊。
         */
        @Override
        public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
            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[] link = buffer.getSpans(off, off, ClickableSpan.class);

                if (link.length != 0) { // 是連結點擊
                    if (widget instanceof LinkTextView) { // 如果實體是LinkTextView
                        ((LinkTextView) widget).linkHit = true;
                    }
                    if (action == MotionEvent.ACTION_UP) { // 放開時
                        link[0].onClick(widget); // 開啟連結
                    } else if (action == MotionEvent.ACTION_DOWN) { // 按下時
                        Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0])); // 選擇連結
                    }
                    return true;
                } else { // 不是連結點擊
                    if (widget instanceof LinkTextView) { // 如果實體是LinkTextView
                        ((LinkTextView) widget).linkHit = false;
                    }
                    Selection.removeSelection(buffer);
                    Touch.onTouchEvent(widget, buffer, event);
                    return false;
                }
            }
            return Touch.onTouchEvent(widget, buffer, event);
        }
    }
}

LinkTextView的使用方式和一般的TextView一樣,只不過如果要使其支援超連結功能,一定要手動指定LinkTextViewMovementMethod給LinkTextView,如下:

linkTextView.setMovementMethod(LinkTextViewMovementMethod.getInstance());