1. 程式人生 > >如何修改TextView連結點選實現(包含連結生成與點選原理分析)

如何修改TextView連結點選實現(包含連結生成與點選原理分析)

*這篇文章的主要目的是想要大家學習如何瞭解實現,修改實現,以達到舉一反三,自行解決問題的目的。

某天遇到這麼一個需求:在TextView中的文字連結要支援跳轉,嗯,這個好辦,TextView本身是支援的,我們只用新增一項屬性就可以搞定:

  android:autoLink="web"

在新增後發現確實是有效果了。但是如果我們不想使用系統預設的瀏覽器,而是想要這個地址跳入某個頁面或者自己應用內的瀏覽器該怎麼辦呢?

好,接下來就是我們要實現的步驟。

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

  • 1.瞭解autoLink的實現。
  • 2.修改autoLink的實現。
  • 3.執行&測試

瞭解autoLink的實現

既然我們可以知道設定autoLink屬性就可以實現連結的自動識別與跳轉,那麼我們就從autoLink開始分析。

開啟TextView.java,尋找autoLink的相關配置讀取引數:

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

我們發現,與autoLink有關的是一個名為mAutoLinkMask的成員屬性,那也就是說:所有與autoLink有關的配置都有這個成員屬性脫不了干係。

那我們就可以在整個TextView的實現中尋找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出現的地方並不多,除了基本的get、set方法之外,它出現在了3個地方,分別是:append(CharSequence text, int start, int end)、setText(CharSequence text, BufferType type)和onTouchEvent(MotionEvent event)。

其中,append方法與setText方法都是用於新增文字的方法,也就說,所有填入TextView的文字都會被加上autoLink的功能。這兩個方法內部都呼叫了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.

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

首先對給定的文字進行掃描,然後將所有的連結文字轉換為可點選的連結。如果第二個引數不為空,那麼它還是會將已有的URLSpan移除,來避免一些問題。

然後我們進入這個方法探一探究竟,看看它是怎麼實現的:

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

這個方法做了以下工作:

  • 1.對舊的Span進行移除,我們看到,這裡獲取Span返回的型別是URLSpan,請留意一下,我們待會會看到它很多次。
  • 2.對給定的WEB_URLS、EMAIL_ADDRESSES、PHONE_NUMBERS、MAP_ADDRESSES型別進行連結查詢。
  • 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是個什麼鬼?和我們想了解的有什麼關係?

其實我們才剛剛瞭解到生成,我們應該還沒忘記,TextView的onTouchEvent方法還沒講到,onTouchEvent方法內部也是有mAutoLinkMask標誌的,我們回去看。

在onTouchEvent方法內有很重要的一段:

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

我們這個時候應該明白,那些連結也走的是TextView的onTouchEvent方法,這當然是理所當然的。不過在這裡,連結的點選是通過ClickableSpan的onClick方法實現的,那這裡的ClickableSpan究竟是誰呢?

我們通過查閱文件發現,ClickableSpan的唯一子類就是我們剛剛見過的URLSpan。但這僅僅是我們的猜測,我們還需要通過實際的執行來檢視是否就是URLSpan在作用連結的點選事件。

我們寫一個小小的實現:

    <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" />

然後執行看看TextView的mText的屬性內部組成:
這裡寫圖片描述

我們可以發現在mText的mSpans屬性中的有一個URLSpan的存在。那到此為止點選的處理就確信是URLSpan的作用無疑了。

那我們可以看看URLSpan自己是怎麼實現的:

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

它的實現很簡潔,我們看到了我們想找的onClick方法,就是這處理了我們的連結點選事件了。那麼我們該如何更改呢?

修改autoLink的實現

如果有對熱修復瞭解的話,那麼肯定對修改dexElements不會陌生。在這裡我們也是相同的思路:通過反射將mSpans屬性中URLSpan物件改為我們自己建立的自定義物件。

那麼接下來就是我們的實現過程:

為了方便使用,我們擴充套件一下TextView:新建一個自定義View並繼承TextView,我們將這個自定義View命名為:AutoLinkTextView。

我們在它的構造方法內分別設定WEB屬性,否則不會自動識別網址連結。

程式碼實現如下:

    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這個標誌屬性出現在了append(CharSequence text, int start, int end)及setText(CharSequence text, BufferType type)這兩個方法內。所以,我們需要對這兩個方法進行擴充套件。

在AutoLinkTextView的類中複寫這兩個方法:

    @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();
    }

這兩個方法除了呼叫基類的方法之外,還呼叫了一個名為replace的方法。這個方法就是接下來我們對原有的URLSpan進行替換的地方。

replace()方法的實現如下:

    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個位置不穩妥,實際環境可能會有多個連結地址
                        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,這裡我們將這個自定義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();
            }
        }
    }
}

在上面的方法中提到了一個ExtendUrlSpan類,這是我們自己寫的擴充套件類,用於定義自己的實現。程式碼如下:

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

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

    @Override
    public void onClick(View widget) {
        //這個方法會在點選連結的時候呼叫,可以實現自定義事件
        Toast.makeText(widget.getContext(), getURL(), Toast.LENGTH_SHORT).show();       
    }
}

為了示例說明,這裡在點選時顯示了一個吐司,吐司的內容是點選的連結地址。

到此為止,我們更改結束。接下來看執行效果。

執行&測試

我們將原有的TextView更換為剛剛實現的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" />

啟動,執行:

這裡寫圖片描述

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