如何修改TextView鏈接點(diǎn)擊實(shí)現(xiàn)(包含鏈接生成與點(diǎn)擊原理分析)

** 這篇文章的主要目的是想要大家學(xué)習(xí)如何了解實(shí)現(xiàn),修改實(shí)現(xiàn),以達(dá)到舉一反三,自行解決問題的目的。*

某天遇到這么一個(gè)需求:在TextView中的文本鏈接要支持跳轉(zhuǎn),嗯,這個(gè)好辦,TextView本身是支持的,我們只用添加一項(xiàng)屬性就可以搞定:

  android:autoLink="web"

在添加后發(fā)現(xiàn)確實(shí)是有效果了。但是如果我們不想使用系統(tǒng)默認(rèn)的瀏覽器,而是想要這個(gè)地址跳入某個(gè)頁面或者自己應(yīng)用內(nèi)的瀏覽器該怎么辦呢?

好,接下來就是我們要實(shí)現(xiàn)的步驟。

俗話說,知己知彼,百戰(zhàn)不殆。所以將我們的步驟分為兩步:

  • 1.了解autoLink的實(shí)現(xiàn)。
  • 2.修改autoLink的實(shí)現(xiàn)。
  • 3.運(yùn)行&測試

了解autoLink的實(shí)現(xiàn)

既然我們可以知道設(shè)置autoLink屬性就可以實(shí)現(xiàn)鏈接的自動識別與跳轉(zhuǎn),那么我們就從autoLink開始分析。

打開TextView.java,尋找autoLink的相關(guān)配置讀取參數(shù):

            case com.android.internal.R.styleable.TextView_autoLink:
                mAutoLinkMask = a.getInt(attr, 0);
                break;

我們發(fā)現(xiàn),與autoLink有關(guān)的是一個(gè)名為mAutoLinkMask的成員屬性,那也就是說:所有與autoLink有關(guān)的配置都有這個(gè)成員屬性脫不了干系。

那我們就可以在整個(gè)TextView的實(shí)現(xiàn)中尋找mAutoLinkMask的身影:


    public void append(CharSequence text, int start, int end) {
        if (!(mText instanceof Editable)) {
            setText(mText, BufferType.EDITABLE);
        }

        ((Editable) mText).append(text, start, end);

        if (mAutoLinkMask != 0) {
            boolean linksWereAdded = Linkify.addLinks((Spannable) mText, mAutoLinkMask);
            if (linksWereAdded && mLinksClickable && !textCanBeSelected()) {
                setMovementMethod(LinkMovementMethod.getInstance());
            }
        }
    }

    ...

    private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {

        ...

        if (mAutoLinkMask != 0) {
            Spannable s2;

            if (type == BufferType.EDITABLE || text instanceof Spannable) {
                s2 = (Spannable) text;
            } else {
                s2 = mSpannableFactory.newSpannable(text);
            }

            if (Linkify.addLinks(s2, mAutoLinkMask)) {
                text = s2;
                type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;

                /*
                 * We must go ahead and set the text before changing the
                 * movement method, because setMovementMethod() may call
                 * setText() again to try to upgrade the buffer type.
                 */
                mText = text;

                // Do not change the movement method for text that support text selection as it
                // would prevent an arbitrary cursor displacement.
                if (mLinksClickable && !textCanBeSelected()) {
                    setMovementMethod(LinkMovementMethod.getInstance());
                }
            }
        }

        ...
    }

    ...

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        ...

            if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
                // The LinkMovementMethod which should handle taps on links has not been installed
                // on non editable text that support text selection.
                // We reproduce its behavior here to open links for these.
                ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
                        getSelectionEnd(), ClickableSpan.class);

                if (links.length > 0) {
                    links[0].onClick(this);
                    handled = true;
                }
            }

        ...

        return superResult;
    }

mAutoLinkMask出現(xiàn)的地方并不多,除了基本的get、set方法之外,它出現(xiàn)在了3個(gè)地方,分別是:append(CharSequence text, int start, int end)、setText(CharSequence text, BufferType type)和onTouchEvent(MotionEvent event)。

其中,append方法與setText方法都是用于添加文本的方法,也就說,所有填入TextView的文本都會被加上autoLink的功能。這兩個(gè)方法內(nèi)部都調(diào)用了Linkify.addLinks(Spannable text, int mask)方法。

Linkify.addLinks(Spannable text, int mask)的注釋是這么寫的:

Scans the text of the provided Spannable and turns all occurrences of the link types indicated in the mask into clickable links. If the mask is nonzero, it also removes any existing URLSpans attached to the Spannable, to avoid problems if you call it repeatedly on the same text.

這段話說了什么呢,翻譯一下:

首先對給定的文本進(jìn)行掃描,然后將所有的鏈接文本轉(zhuǎn)換為可點(diǎn)擊的鏈接。如果第二個(gè)參數(shù)不為空,那么它還是會將已有的URLSpan移除,來避免一些問題。

然后我們進(jìn)入這個(gè)方法探一探究竟,看看它是怎么實(shí)現(xiàn)的:

    public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
        if (mask == 0) {
            return false;
        }

        URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);

        for (int i = old.length - 1; i >= 0; i--) {
            text.removeSpan(old[i]);
        }

        ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();

        if ((mask & WEB_URLS) != 0) {
            gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL,
                new String[] { "http://", "https://", "rtsp://" },
                sUrlMatchFilter, null);
        }

        if ((mask & EMAIL_ADDRESSES) != 0) {
            gatherLinks(links, text, Patterns.AUTOLINK_EMAIL_ADDRESS,
                new String[] { "mailto:" },
                null, null);
        }

        if ((mask & PHONE_NUMBERS) != 0) {
            gatherTelLinks(links, text);
        }

        if ((mask & MAP_ADDRESSES) != 0) {
            gatherMapLinks(links, text);
        }

        pruneOverlaps(links);

        if (links.size() == 0) {
            return false;
        }

        for (LinkSpec link: links) {
            applyLink(link.url, link.start, link.end, text);
        }

        return true;
    }

這個(gè)方法做了以下工作:

  • 1.對舊的Span進(jìn)行移除,我們看到,這里獲取Span返回的類型是URLSpan,請留意一下,我們待會會看到它很多次。
  • 2.對給定的WEB_URLS、EMAIL_ADDRESSES、PHONE_NUMBERS、MAP_ADDRESSES類型進(jìn)行鏈接查找。
  • 3.生成新的Span。

這是最后生成新的Span的方法,它這里用了URLSpan:

    private static final void applyLink(String url, int start, int end, Spannable text) {
        URLSpan span = new URLSpan(url);

        text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

這里的URLSpan是個(gè)什么鬼?和我們想了解的有什么關(guān)系?

其實(shí)我們才剛剛了解到生成,我們應(yīng)該還沒忘記,TextView的onTouchEvent方法還沒講到,onTouchEvent方法內(nèi)部也是有mAutoLinkMask標(biāo)志的,我們回去看。

在onTouchEvent方法內(nèi)有很重要的一段:

            if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
                ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
                        getSelectionEnd(), ClickableSpan.class);

                if (links.length > 0) {
                    links[0].onClick(this);
                    handled = true;
                }
            }

我們這個(gè)時(shí)候應(yīng)該明白,那些鏈接也走的是TextView的onTouchEvent方法,這當(dāng)然是理所當(dāng)然的。不過在這里,鏈接的點(diǎn)擊是通過ClickableSpan的onClick方法實(shí)現(xiàn)的,那這里的ClickableSpan究竟是誰呢?

我們通過查閱文檔發(fā)現(xiàn),ClickableSpan的唯一子類就是我們剛剛見過的URLSpan。但這僅僅是我們的猜測,我們還需要通過實(shí)際的運(yùn)行來查看是否就是URLSpan在作用鏈接的點(diǎn)擊事件。

我們寫一個(gè)小小的實(shí)現(xiàn):

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:autoLink="web"
        android:text="Hello! https://developer.android.google.cn/reference/android/text/style/ClickableSpan.html" />

然后運(yùn)行看看TextView的mText的屬性內(nèi)部組成:


這里寫圖片描述

我們可以發(fā)現(xiàn)在mText的mSpans屬性中的有一個(gè)URLSpan的存在。那到此為止點(diǎn)擊的處理就確信是URLSpan的作用無疑了。

那我們可以看看URLSpan自己是怎么實(shí)現(xiàn)的:

public class URLSpan extends ClickableSpan implements ParcelableSpan {

    private final String mURL;

    public URLSpan(String url) {
        mURL = url;
    }

    public URLSpan(Parcel src) {
        mURL = src.readString();
    }

    public int getSpanTypeId() {
        return TextUtils.URL_SPAN;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mURL);
    }

    public String getURL() {
        return mURL;
    }

    @Override
    public void onClick(View widget) {
        Uri uri = Uri.parse(getURL());
        Context context = widget.getContext();
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
        context.startActivity(intent);
    }
}

它的實(shí)現(xiàn)很簡潔,我們看到了我們想找的onClick方法,就是這處理了我們的鏈接點(diǎn)擊事件了。那么我們該如何更改呢?

修改autoLink的實(shí)現(xiàn)

如果有對熱修復(fù)了解的話,那么肯定對修改dexElements不會陌生。在這里我們也是相同的思路:通過反射將mSpans屬性中URLSpan對象改為我們自己創(chuàng)建的自定義對象。

那么接下來就是我們的實(shí)現(xiàn)過程:

為了方便使用,我們擴(kuò)展一下TextView:新建一個(gè)自定義View并繼承TextView,我們將這個(gè)自定義View命名為:AutoLinkTextView。

我們在它的構(gòu)造方法內(nèi)分別設(shè)置WEB屬性,否則不會自動識別網(wǎng)址鏈接。

代碼實(shí)現(xiàn)如下:

    public AutoLinkTextView(Context context) {
        super(context);
        setAutoLinkMask(Linkify.WEB_URLS);
    }

    public AutoLinkTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setAutoLinkMask(Linkify.WEB_URLS);
    }

    public AutoLinkTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setAutoLinkMask(Linkify.WEB_URLS);
    }

好,做好了鋪墊之后,我們在上面了解到,mAutoLinkMask這個(gè)標(biāo)志屬性出現(xiàn)在了append(CharSequence text, int start, int end)及setText(CharSequence text, BufferType type)這兩個(gè)方法內(nèi)。所以,我們需要對這兩個(gè)方法進(jìn)行擴(kuò)展。

在AutoLinkTextView的類中復(fù)寫這兩個(gè)方法:

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        replace();
    }

    @Override
    public void append(CharSequence text, int start, int end) {
        super.append(text, start, end);
        replace();
    }

這兩個(gè)方法除了調(diào)用基類的方法之外,還調(diào)用了一個(gè)名為replace的方法。這個(gè)方法就是接下來我們對原有的URLSpan進(jìn)行替換的地方。

replace()方法的實(shí)現(xiàn)如下:

    private void replace() {
        CharSequence text = getText();
        
        if (text instanceof SpannableString) {
            SpannableString spannableString = (SpannableString) text;
            Class<? extends SpannableString> aClass = spannableString.getClass();

            try {
                //mSpans屬性屬于SpannableString的父類成員
                Class<?> aClassSuperclass = aClass.getSuperclass();
                Field mSpans = aClassSuperclass.getDeclaredField("mSpans");
                mSpans.setAccessible(true);
                Object o = mSpans.get(spannableString);

                if (o.getClass().isArray()) {
                    Object objs[] = (Object[]) o;

                    if (objs.length > 1) {
                        //這里的第0個(gè)位置不穩(wěn)妥,實(shí)際環(huán)境可能會有多個(gè)鏈接地址
                        Object obj = objs[0];
                        if (obj.getClass().equals(URLSpan.class)) {
                        
                            //獲取URLSpan的mURL值,用于新的URLSpan的生成
                            Field oldUrlField = obj.getClass().getDeclaredField("mURL");
                            oldUrlField.setAccessible(true);
                            Object o1 = oldUrlField.get(obj);

                            //生成新的自定義的URLSpan,這里我們將這個(gè)自定義URLSpan命名為ExtendUrlSpan
                            Constructor<?> constructor = ExtendUrlSpan.class.getConstructor(String.class);
                            constructor.setAccessible(true);
                            Object newUrlField = constructor.newInstance(o1.toString());
                            
                            //替換
                            objs[0] = newUrlField;
                        }
                    }
                }
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
}

在上面的方法中提到了一個(gè)ExtendUrlSpan類,這是我們自己寫的擴(kuò)展類,用于定義自己的實(shí)現(xiàn)。代碼如下:

public class ExtendUrlSpan extends URLSpan {
    public ExtendUrlSpan(String url) {
        super(url);
    }

    public ExtendUrlSpan(Parcel src) {
        super(src);
    }

    @Override
    public void onClick(View widget) {
        //這個(gè)方法會在點(diǎn)擊鏈接的時(shí)候調(diào)用,可以實(shí)現(xiàn)自定義事件
        Toast.makeText(widget.getContext(), getURL(), Toast.LENGTH_SHORT).show();       
    }
}

為了示例說明,這里在點(diǎn)擊時(shí)顯示了一個(gè)吐司,吐司的內(nèi)容是點(diǎn)擊的鏈接地址。

到此為止,我們更改結(jié)束。接下來看運(yùn)行效果。

運(yùn)行&測試

我們將原有的TextView更換為剛剛實(shí)現(xiàn)的AutoLinkTextView:

    <com.sahadev.support.AutoLinkTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:autoLink="web"
        android:text="Hello! https://developer.android.google.cn/reference/android/text/style/ClickableSpan.html" />

啟動,運(yùn)行:

這里寫圖片描述

這說明我們的更改是生效的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容