国际惯例,先放github:
https://github.com/razerdp/HttpUrlTextView
或者apkbus:
http://www.apkbus.com/forum.php?mod=viewthread&tid=248196
需求:
1 - 能够识别出一大堆文字里面的url
2 - 能够识别出非http://|https://|ftp://等协议开头的非标准url
3 - 对于原来已经存在clickspan的文字不能去掉其点击事件
4 - url可点击
分析:
虽然android的textview自带了autoLink属性,但是用过的人都知道,两个链接之间的文字,或者有时候文字带着链接会导致整一个文字都变成可点击的链接,这对用户体验并不好。因此就有了这次的定制。
针对第一点,和第二点,其实两者都一样,要识别出一大堆东东里面的某个小东西,正则是我想到的比较好的解决方法,而且网络上有很多正则相关的,经受过考验的表达式(虽说有正则就必定有绕过。。。。)
针对第三点,我们可以通过span.getSpans来获取每个span然后手动拼接起来。
针对第四点,没啥好说的,直接用ClickableSpan解决
开工:
- 一、
能够识别出一大堆文字里面的url
能够识别出非http://|https://|ftp://等协议开头的非标准url
-
首先上网查查识别url的正则表达式,毕竟不制造重复的轮子是我们的宗旨←_←
-
于是乎,咱们就得到了这一个:
-
((http|ftp|https)://)(([a-zA-Z0-9\._-]+\.[a-zA-Z]{2,6})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\&%_\./-~-]*)?
-
-
初看了一下,是不是有很明显的http等协议头,现在当作咱们不了解正则(事实上我还真不是非常了解),然后看了看,一大堆东西干毛啊!!!
-
-
不过不看不行啊,,,,orz于是只能硬着头皮分析一下,正则的语法咱们就不说了,网上一大堆,看一下这个正则表达式,有木有发现有一个符号我们很熟悉?就是这个→“|”,写android,准确的说咱们写代码的时候应该经常用到“||”这个符号,代表或者的意思,这里也是,这个“|”符号就是意味着匹配“|”前的正则或者匹配“|”后的正则
-
-
那也就是意味着,在http等协议头后面的,就是url,也就是aaa.bbb.ccc/ddd/eee/fff......,这也就符合了需求的第二点,于是咱们就改了一下这个表达式(其实就是ctrl +c/v 大法)
-
就变成了这样:
-
((http|ftp|https)://)(([a-zA-Z0-9\._-]+\.[a-zA-Z]{2,6})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\&%_\./-~-]*)?|(([a-zA-Z0-9\._-]+\.[a-zA-Z]{2,6})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\&%_\./-~-]*)?
-
-
但在java里面的字符串中要表示"\"这个符号,咱们就得改一下,因为“\”这个是转义符,所以要匹配"\"这个符号,咱们就得把"\"换成"\\"
-
于是乎,在java里面,咱们的正则就变成了这样:
-
private String pattern = "((http|ftp|https)://)(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\\&%_\\./-~-]*)?|(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\\&%_\\./-~-]*)?";
-
OK,现在咱们有了正则,接下来就是构造正则匹配器Pattern和Matcher了,这两个也很简单,创建一个pattern,传入正则,而matcher则是由pattern弄出来的
-
定义变量:
-
// 创建 Pattern 对象 Pattern r = Pattern.compile(pattern); // 现在创建 matcher 对象 Matcher m;
-
这样咱们的需求1和2就完成了,接下来完成需求3
-
-
需求三则需要在匹配的时候弄,咱们就先不考虑,待会再写
-
对于需求四,也没什么好说的,跟需求三一起完成。
-
对于需求三,其实咱们的最主要目的是把含有span的文字抽取出来(ps:这里因为我的偷懒,会有一个bug,就是最开始的span到最后一个span之间的文字被当作span而抽取出来,并不会触发正则匹配,这个待会解释)
-
大概流程就是这样:
-
文字输入(setText)->是否需要匹配,否,则直接setText,是,则 ->抽取span ->抽取剩余内容作为url正则匹配 ->匹配成功,则赋予clickspan,不成功,则拼接 ->将之前的span拼接 ->再次调用setText
-
-
-
代码:
-
package widget.textviewforurl; import android.content.Context; import android.text.Spanned; import android.text.TextPaint; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; import android.widget.Toast; import java.util.LinkedList; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Created by 大灯泡 on 2015/11/4. * 识别url的textView */ public class HttpTextView extends TextView { //测试文字 public String testText = "1.测试测试测试google.cn测试曹娥U去我如\n" + "2.侧首IU包宿123124 baidu.com报道锁人副I我去额555\n" + "3.博啊us豆腐啊哦I吧安静哦.博爱us都I人.dsaboauo www.weiju.ba/xx2/b54\n" + "4.这是一个测试哟http://www.baidu.com,这是测试哟\n" + "5.测试测试啊是的赴欧 我们的网址是:qq.164701463.net测试测试哟\n" + "6.的撒发吧额听歌:https://xx.125.com 654987打飞机阿伯I安\n" + "7.把儿童的方向:ftp://4399.com多发生部位,大师傅帮你\n" + "8.这次是个多网址哟 www.baidu.com哈哈哈www.google.com垃圾都是泪放假啊是的佛I 8264.com\n" + "9.你敢相信这是一个测试?www.baidu.com/?html=12354bhb35&ask=dasoiubao\n" + "10.这是一个下载地址哟 www.baidu.com/img/xxxx.jpg\n" + "11.baidu.com这个地址在开头\n" + "12.这个地址在末尾baidu.com\n" + "13.这是文字加地址加哈哈baba.ba 微笑掉地赴澳IU发qq.com微笑"; /* * 正则文本 * ((http|ftp|https)://)(([a-zA-Z0-9\._-]+\.[a-zA-Z]{2,6})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\&%_\./-~-]*)?|(([a-zA-Z0-9\._-]+\.[a-zA-Z]{2,6})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\&%_\./-~-]*)? * */ private String pattern = "((http|ftp|https)://)(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\\&%_\\./-~-]*)?|(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\\&%_\\./-~-]*)?"; // 创建 Pattern 对象 Pattern r = Pattern.compile(pattern); // 现在创建 matcher 对象 Matcher m; //记录网址的list LinkedList<String> mStringList; //记录该网址所在位置的list LinkedList<UrlInfo> mUrlInfos; int flag=Spanned.SPAN_POINT_MARK; private boolean needToRegionUrl = true;//是否开启识别URL,默认开启 public HttpTextView(Context context) { this(context, null); } public HttpTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public HttpTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mStringList = new LinkedList<>(); mUrlInfos = new LinkedList<>(); } public void setUrlText(CharSequence text) { if (needToRegionUrl) { SpannableStringBuilderAllVer stringBuilderAllVer = recognUrl(text); super.setText(stringBuilderAllVer); this.setMovementMethod(LinkMovementMethod.getInstance()); } else { super.setText(text); } } public boolean getIsNeedToRegionUrl() { return needToRegionUrl; } public void setOpenRegionUrl(boolean needToRegionUrl) { this.needToRegionUrl = needToRegionUrl; } private SpannableStringBuilderAllVer recognUrl(CharSequence text) { mStringList.clear(); mUrlInfos.clear(); CharSequence contextText; CharSequence clickText; text = text == null ? "" : text; //以下用于拼接本来存在的spanText SpannableStringBuilderAllVer span = new SpannableStringBuilderAllVer(text); ClickableSpan[] clickableSpans = span.getSpans(0, text.length(), ClickableSpan.class); if (clickableSpans.length > 0) { int start=0; int end=0; for (int i=0;i<clickableSpans.length;i++){ start=span.getSpanStart(clickableSpans[0]); end=span.getSpanEnd(clickableSpans[i]); } //可点击文本后面的内容页 contextText = text.subSequence(end, text.length()); //可点击文本 clickText = text.subSequence(start, end); } else { contextText = text; clickText = null; } m = r.matcher(contextText); //匹配成功 while (m.find()) { //得到网址数 UrlInfo info = new UrlInfo(); info.start = m.start(); info.end = m.end(); mStringList.add(m.group()); mUrlInfos.add(info); } return jointText(clickText, contextText); } /** 拼接文本 */ private SpannableStringBuilderAllVer jointText(CharSequence clickSpanText, CharSequence contentText) { SpannableStringBuilderAllVer spanBuilder; if (clickSpanText != null) { spanBuilder = new SpannableStringBuilderAllVer(clickSpanText); } else { spanBuilder = new SpannableStringBuilderAllVer(); } if (mStringList.size() > 0) { //只有一个网址 if (mStringList.size() == 1) { String preStr = contentText.toString().substring(0, mUrlInfos.get(0).start); spanBuilder.append(preStr); String url = mStringList.get(0); spanBuilder.append(url, new URLClick(url), flag); String nextStr = contentText.toString().substring(mUrlInfos.get(0).end); spanBuilder.append(nextStr); } else { //有多个网址 for (int i = 0; i < mStringList.size(); i++) { if (i == 0) { //拼接第1个span的前面文本 String headStr = contentText.toString().substring(0, mUrlInfos.get(0).start); spanBuilder.append(headStr); } if (i == mStringList.size() - 1) { //拼接最后一个span的后面的文本 spanBuilder.append(mStringList.get(i), new URLClick(mStringList.get(i)), flag); String footStr = contentText.toString().substring(mUrlInfos.get(i).end); spanBuilder.append(footStr); } if (i != mStringList.size() - 1) { //拼接两两span之间的文本 spanBuilder.append(mStringList.get(i), new URLClick(mStringList.get(i)), flag); String betweenStr = contentText.toString() .substring(mUrlInfos.get(i).end, mUrlInfos.get(i + 1).start); spanBuilder.append(betweenStr); } } } } else { spanBuilder.append(contentText); } return spanBuilder; } //------------------------------------------定义----------------------------------------------- class UrlInfo { public int start; public int end; } class URLClick extends ClickableSpan { private String text; public URLClick(String text) { this.text = text; } @Override public void onClick(View widget) { Toast.makeText(widget.getContext(),text,Toast.LENGTH_SHORT).show(); } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(0xff517fae); ds.setUnderlineText(false); } } }
通过代码不难看出,咱们的核心方法在于
这个两个方法用于匹配以及拼接文字recognUrl(CharSequence text)以及jointText(CharSequence clickSpanText,
期中recognUrl()方法大部分的注释都有,但这个方法要注意
这个方法是用于得到文字中含有的clickspanClickableSpan[] clickableSpans = span.getSpans(0, text.length(), ClickableSpan.class);
-
然后下面的if方法则是用于获取clickspan文本后面的文字
-
从if (clickableSpans.length > 0) { int start=0; int end=0; for (int i=0;i<clickableSpans.length;i++){ start=span.getSpanStart(clickableSpans[0]); end=span.getSpanEnd(clickableSpans[i]); } //可点击文本后面的内容页 contextText = text.subSequence(end, text.length()); //可点击文本 clickText = text.subSequence(start, end); } else { contextText = text; clickText = null; }
可以看到,我获取的非clickspan文本是整个text的最后一个clickspan之后的文字,这意味着最后一个clickspan前面的文字我们是不做匹配的,我这么做的原因是在项目中我们的一个需求是评论控件,也就是A回复B,其中A是一个clickspan,B也是,而这个评论控件需要匹配网址,也就是这样的效果:start=span.getSpanStart(clickableSpans[0]); end=span.getSpanEnd(clickableSpans[i]);
-
A回复B:www.baidu.com,这个文字里面A和B和www.baidu.com都是可点击的,并且执行不同的操作。因此就不做A和B之间的正则匹配了。
-
-
另外,大家可能看到
-
SpannableStringBuilderAllVer
-
这个类是继承于SpannableStringBuilder,但其中有一个方法在API21以下不支持,因此单独抽出来:
-
package widget.textviewforurl; import android.text.SpannableStringBuilder; /** * Created by 大灯泡 on 2015/9/30. * 兼容低版本SpannableStringBuilder */ public class SpannableStringBuilderAllVer extends SpannableStringBuilder{ public SpannableStringBuilderAllVer() { super(""); } public SpannableStringBuilderAllVer(CharSequence text) { super(text, 0, text.length()); } public SpannableStringBuilderAllVer(CharSequence text, int start, int end){ super(text,start,end); } public SpannableStringBuilderAllVer append(CharSequence text) { if (text == null) return this; int length = length(); return (SpannableStringBuilderAllVer)replace(length, length, text, 0, text.length()); } /**该方法在原API里面只支持API21或者以上,这里抽取出来以适应低版本*/ public SpannableStringBuilderAllVer append(CharSequence text, Object what, int flags) { if (text == null) return this; int start = length(); append(text); setSpan(what, start, length(), flags); return this; } }
至于剩下的一个joinText,在代码里面也注释的很清楚了,这里就不再阐述。
这个简单的小控件仍然有需要完善的地方,如果大家有更好的方法,欢迎讨论哈-V-另外,在这里扎根,一步一步见证自己进步的脚印-V-【END】