** 这篇文章的主要目的是想要大家学习如何了解实现,修改实现,以达到举一反三,自行解决问题的目的。*
某天遇到这么一个需求:在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" />
启动,运行:
这说明我们的更改是生效的。