如何修改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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/478732.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode 752. 打开转盘锁(图的BFS最短路径)

1. 题目 你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字&#xff1a; ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转&#xff1a;例如把 ‘9’ 变为 ‘0’&#xff0c;‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨…

使用TensorFlow训练WDL模型性能问题定位与调优

简介 TensorFlow是Google研发的第二代人工智能学习系统&#xff0c;能够处理多种深度学习算法模型&#xff0c;以功能强大和高可扩展性而著称。TensorFlow完全开源&#xff0c;所以很多公司都在使用&#xff0c;但是美团点评在使用分布式TensorFlow训练WDL模型时&#xff0c;发…

docker镜像打包save,载入load,启动run

docker镜像打包save&#xff0c;载入load&#xff0c;启动run docker打包&#xff0c;针对的是镜像&#xff0c;而不是运行中的容器。 查看当前系统的镜像文件&#xff1a; docker images 首先&#xff0c;将当前运行中的&#xff0c;已经自定义修改之后的容器保存为新的镜像…

会议交流 - CNCC 技术论坛 | NLP中知识和数据怎么选?当然是全都要!——第四届中文信息技术发展战略研讨会...

本文转载自公众号&#xff1a;中国计算机学会 。本论坛将于CNCC2020期间&#xff0c;10月23日下午16:00-18:00&#xff0c;在北京新世纪日航饭店3层南京厅举行。本论坛将邀请多位来自国内著名高校和人工智能企业的知名自然语言处理专家介绍他们在知识图谱、知识获取、预训练模型…

从我开发过的Tensorflow、飞桨、无量框架看深度学习这几年

文 | Peter潘欣知乎和深度学习框架打交道已有多年时间。从Google的TensorFlow, 到百度的PaddlePaddle&#xff0c;再到现在腾讯的无量。很庆幸在AI技术爆发的这些年横跨中美几家公司&#xff0c;站在一个比较好的视角看着世界发生巨大的变化。在这些经历中&#xff0c;视角在不…

探索Glide对Gif图片资源的获取、解析过程

先预祝大家汤圆节快乐&#xff01;很久没写博客了。今天我们来探索一下Glide是如何支持Gif图片加载的。 本篇博客的目的 了解代码分析的基本思路与方法了解Glide是如何对Gif图片进行支持的 探索背景 为什么会有这么一个想法呢&#xff0c;一来一直对Glide是知其名而不知其所…

美团“猜你喜欢”深度学习排序模型实践

引言 推荐作为解决信息过载和挖掘用户潜在需求的技术手段&#xff0c;在美团点评这样业务丰富的生活服务电子商务平台&#xff0c;发挥着重要的作用。在美团App里&#xff0c;首页的“猜你喜欢”、运营区、酒店旅游推荐等重要的业务场景&#xff0c;都是推荐的用武之地。 目前&…

如何关闭docker容器里的进程

如何关闭docker容器里的进程 1、使用docker exec 容器名 ps -ef命令查看进程信息 示例&#xff1a; 创建名为"redis"的容器&#xff0c;并在容器内部和宿主机中查看容器中的进程信息&#xff1a; 2、然后进入该容器中&#xff0c;执行如下命令即可停掉进程&…

论文浅尝 | 一日看尽长安花--NLP可解释研究梳理

本文是对TACL2019《Analysis Methods in Neural Language Processing: A Survey》的翻译和整理。本文转载自知乎&#xff0c;文章链接&#xff1a;https://zhuanlan.zhihu.com/p/265815975导读NLP领域发展迅猛&#xff0c;其模型分析方法也正逐渐火热。为什么要研究NLP的可解释…

简明扼要的反射入门教程

反射 反射作为RTTI语言&#xff08;比如Java&#xff09;的基础之一被很多人所熟知&#xff0c;但是有些同学对反射本身还是懵懵懂懂的&#xff0c;不是很清楚它到底有什么用。今天这节课我们就对反射本身来一个通体的认知。 定义 反射所在的包为&#xff1a;java.lang.refl…

人在斯坦福,刚上CS224n

文 | Jazon大家好&#xff0c;我是Jazon&#xff0c;现在是Stanford计算机硕士项目的一只学生&#xff0c;非常荣幸加入小夕的大家庭&#xff01;请各路大神多多指教呀。2021年1月12日&#xff0c;又一季Stanford CS224n——自然语言处理开课了&#xff0c;我和很多MSCS同学一起…

美团外卖骑手背后的AI技术

背景 随着数字化时代的到来&#xff0c;外卖市场近年来发展非常迅猛。对外卖物流系统而言&#xff0c;配送效率和用户体验至关重要。而实际配送过程是由配送员&#xff08;骑手&#xff09;最终完成的&#xff0c;因此&#xff0c;想要真正提升配送效率&#xff0c;不但要在智能…

软件设计师:下午试题类型以及问题总结+软件设计师考试下午题统计分析(06到2019年)

‘’’ 首次更新下午试题 ‘’’ 下午考试时间为150分钟&#xff08;14:00–16:30&#xff09; 试题一到四为必答题&#xff0c;试题五和六为选答一个&#xff0c;每题15分&#xff0c;总共75分&#xff0c;每题大概3小问 第一题为结构化分析与设计&#xff0c;考查数据流…

LeetCode 494. 目标和(DFS+DP)

文章目录1. 题目2. 解题2.1 递归2.2 DP1. 题目 给定一个非负整数数组&#xff0c;a1, a2, …, an, 和一个目标数&#xff0c;S。现在你有两个符号 和 -。对于数组中的任意一个整数&#xff0c;你都可以从 或 -中选择一个符号添加在前面。 返回可以使最终数组和为目标数 S 的…

使用Kotlin开发Android应用初体验

使用Kotlin开发Android应用初体验 昨晚&#xff0c;最近一届的谷歌IO大会正式将Kotlin确定为了官方开发语言&#xff0c;作为一名Android开发鸟&#xff0c;怎么能不及时尝尝鲜呢? Kotlin的简要介绍 在开发之前&#xff0c;很多同学一定有很多疑问&#xff0c;Kotlin到底有…

论文浅尝 - ICML2020 | 基于子图推理的归纳式关系预测

论文笔记整理&#xff1a;陈名杨&#xff0c;浙江大学在读博士生&#xff0c;主要研究方向为知识图谱表示学习。论文来源&#xff1a;ICML 2020Introduction当前在知识图谱上&#xff08;KnowledgeGraph&#xff0c;KG&#xff09;进行关系预测的很多方法都依靠在对知识图谱中的…

CVPR 二十年,影响力最大的 10 篇论文!

文 | 二玖极市平台此前&#xff0c;极市盘点了图像分割在过去二十年中影响力最大的10篇论文&#xff0c;得到了许多开发者的支持。今天&#xff0c;我们将对计算机视觉领域三大顶会之一CVPR在近二十年来中产生的优秀论文进行一个全面的盘点与总结。CVPR是计算机视觉领域三大顶会…

DataMan-美团旅行数据质量监管平台实践

背景 数据&#xff0c;已经成为互联网企业非常依赖的新型重要资产。数据质量的好坏直接关系到信息的精准度&#xff0c;也影响到企业的生存和竞争力。Michael Hammer&#xff08;《Reengineering the Corporation》一书的作者&#xff09;曾说过&#xff0c;看起来不起眼的数据…

jupyter notebook切换到其他配置好的conda虚拟环境

1 手把手教你如何把jupyter notebook切换到其他配置好的conda虚拟环境 https://blog.csdn.net/weixin_41813895/article/details/84750990 2 Jupyter notebook切换Python环境 https://www.jianshu.com/p/8188c32a3a34

LeetCode 133. 克隆图(图的BFS/DFS)

1. 题目 给定无向连通图中一个节点的引用&#xff0c;返回该图的深拷贝&#xff08;克隆&#xff09;。图中的每个节点都包含它的值 val&#xff08;Int&#xff09; 和其邻居的列表&#xff08;list[Node]&#xff09;。 class Node { public:int val;vector<Node*> n…