简单定制Android控件(1) - 自识别url的TextView

本文介绍了如何在Android中自定义TextView,使其能识别并高亮显示非标准URL。通过正则表达式实现URL的匹配,并保留原有点击事件。详细讲述了正则匹配URL的过程以及在Java中使用正则的注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

国际惯例,先放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()方法大部分的注释都有,但这个方法要注意
    ClickableSpan[] clickableSpans = span.getSpans(0, text.length(), ClickableSpan.class);
    这个方法是用于得到文字中含有的clickspan
  • 然后下面的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;
            }
     start=span.getSpanStart(clickableSpans[0]);
     end=span.getSpanEnd(clickableSpans[i]);
    可以看到,我获取的非clickspan文本是整个text的最后一个clickspan之后的文字,这意味着最后一个clickspan前面的文字我们是不做匹配的,我这么做的原因是在项目中我们的一个需求是评论控件,也就是A回复B,其中A是一个clickspan,B也是,而这个评论控件需要匹配网址,也就是这样的效果:

  • 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】



评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值