如何修改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());}}}...}...@Overridepublic 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;}@Overridepublic 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的類中復寫這兩個方法:
@Overridepublic void setText(CharSequence text, BufferType type) {super.setText(text, type);replace();}@Overridepublic 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命名為ExtendUrlSpanConstructor<?> 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);}@Overridepublic 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" />啟動,運行:
這說明我們的更改是生效的。
*項目的相關地址為:https://code.csdn.net/u011064099/android-textview-autolink-click-reflect/tree/master
總結
以上是生活随笔為你收集整理的如何修改TextView链接点击实现(包含链接生成与点击原理分析)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tensorflow版本升级后的各种方法
- 下一篇: 工程实践:基于规则句法的事件关系与主谓宾