TextView的autoLink属性设置超链接问题

需求如下:用一个view展示"请点击https://mp.csdn.net进行查找"这句话,并点击链接地址时可以进行跳转。
最近接到类似的这种需求,网上查找资料学习到了TextView的autoLink属性,那autoLink是怎么使用的呢?为什么设置autoLink就可以实现TextView的超链接,底层是怎么实现的呢?TextView显示时自动排版不整齐,怎么解决呢?

TextView的autoLink属性的使用

TextView的autoLink的属性可以将符合指定格式的文本转换为可单击的超链接形式,有以下几种格式:
1、none:表示不进行任何匹配,默认;
2、web:表示匹配Web Url,如:内容中的http://www.baidu.com会成为可单击跳转的超链接;
3、email:表示匹配邮件地址:如:邮件地址为hello@com.cn会成为可单击的超链接;
4、phone:表示匹配电话号码:如:点击号码10086会跳到拨号界面;
5、map:表示匹配地图地址;
6、all:表示将会匹配web、email、phone、map;
以Web格式为例,在xml文件中使用:

<TextView
    android:id="@+id/message"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="message"
    android:gravity="center_horizontal"
    android:autoLink="web"/>

运行效果:
展示时:

点击时:用浏览器打开链接地址

这时如果需求要求链接地址显示下划线,显示链接地址颜色为其他颜色,而且要求用app打开链接地址即用webview显示链接?那这时候怎么办呢?
显示效果如下:

需要拦截链接地址,给链接地址设置ClickableSpan样式,具体如下:
链接地址显示下划线:重写ClickableSpan方法,设置ds.setUnderlineText(true);
设置显示链接地址颜色:设置textview的setLinkTextColor属性;
设置链接地址点击事件:重写ClickableSpan的方法onClick自定义链接的点击事件;
例子:给链接地址设置显示下划线,设置显示颜色为绿色,设置点击链接地址时用webview显示
MainActivifty.java中:

private void showToolMethodDialog(){
    View view = LayoutInflater.from(this).inflate(R.layout.dialog_layout2, null);
    TextView textView = view.findViewById(R.id.message);
    TextView close = view.findViewById(R.id.close);
    textView.setText(text);
    textView.setLinkTextColor(getResources().getColor(R.color.color_03bf6d));//设置链接地址显示颜色
    AutoLinKTextViewUtil.getInstance().interceptHyperLink(textView);//拦截链接地址,设置显示样式和点击事件
    final AlertDialog dialog = new AlertDialog.Builder(this).create();
    dialog.setTitle("工具类方式test");
    dialog.setView(view);
    close.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        dialog.cancel();
      }
    });
    dialog.show();
  }

AutoLinkTextViewUtil.java:

public class AutoLinKTextViewUtil {

  private volatile static AutoLinKTextViewUtil autoLinKTextViewUtil;


  private AutoLinKTextViewUtil(){};

  public static AutoLinKTextViewUtil getInstance(){
    if(autoLinKTextViewUtil == null){
      synchronized (AutoLinKTextViewUtil.class){
        if(autoLinKTextViewUtil == null){
          autoLinKTextViewUtil = new AutoLinKTextViewUtil();
        }
      }
    }
    return autoLinKTextViewUtil;
  }

  public void interceptHyperLink(TextView textView) {
    textView.setMovementMethod(LinkMovementMethod.getInstance());
    CharSequence text = textView.getText();
    if (text instanceof Spannable) {
      int end = text.length();
      Spannable spannable = (Spannable) textView.getText();
      URLSpan[] urlSpans = spannable.getSpans(0, end, URLSpan.class);
      if (urlSpans.length == 0) {
        return;
      }

      SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
      // 循环遍历并拦截 所有http://开头的链接
      for (URLSpan uri : urlSpans) {
        String url = uri.getURL();
        if (url.indexOf("http://") == 0) {
          CustomUrlSpan customUrlSpan = new CustomUrlSpan(textView.getContext(), url,
              new CustomUrlSpan.OnClickInterface() {
                @Override
                public void onClick(View widget, String url, Context context) {//处理链接地址的点击事情
                  if(!TextUtils.isEmpty(url)){
                    Toast.makeText(widget.getContext(), url, Toast.LENGTH_SHORT).show();
                    Intent intent = new Intent(widget.getContext(), WebViewActivity.class);
                    intent.putExtra("url", url);
                    widget.getContext().startActivity(intent);
                  }
                }
              });
          spannableStringBuilder.setSpan(customUrlSpan, spannable.getSpanStart(uri),
              spannable.getSpanEnd(uri), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
        }
      }
      textView.setText(spannableStringBuilder);
    }
  }
}

CustomUrlSpan.java:

public class CustomUrlSpan extends ClickableSpan {

  private Context context;
  private String url;
  private OnClickInterface onClickInterface;
  public CustomUrlSpan(Context context,String url, OnClickInterface onClickInterface){
    this.context = context;
    this.url = url;
    this.onClickInterface = onClickInterface;
  }

  @Override
  public void updateDrawState(TextPaint ds) {
    ds.setUnderlineText(true);//设置显示下划线
  }

  @Override
  public void onClick(View widget) {//链接地址点击事件监听
    if(onClickInterface != null){
      onClickInterface.onClick(widget, url, context);
    }
  }

  interface OnClickInterface{
    void onClick(View widget, String url, Context context);
  }

注意:Textview识别不出来字符串中的链接地址时,需要在链接地址前后加空格,例如:
private String text = “1.测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试 http://www.baidu.com 网址”;

从源码入手分析TextView的autoLink属性

为什么设置autoLink属性后就可以给TextView设置超链接呢,为什么重写ClickableSpan的方法就可以设置超链接显示样式和处理链接的点击事情呢?还是从源码入手进行分析:
TextView的源码:
在构造方法中搜索autoLink可以看到:

case com.android.internal.R.styleable.TextView_autoLink:
                    mAutoLinkMask = a.getInt(attr, 0);
                    break;

在构造方法中获取设置的属性autoLink并赋值给了变量mAutoLinkMask,然后我们搜索mAutoLinkMask变量,在setText方法中发现如下代码:

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.
                 */
                setTextInternal(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());
                }
            }
        }
         //省略若干代码
 }

上边代码中有一个Linkify.addLinks(s2, mAutoLinkMask)判断,那么这个方法作用是干什么的呢?
跟代码,查看addLinks方法,如下:

private static boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask,
            @Nullable Context context) {
        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) {//判断是否有web属性
            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, context);
        }

        if ((mask & MAP_ADDRESSES) != 0) {
            gatherMapLinks(links, text);
        }

        pruneOverlaps(links);

        if (links.size() == 0) {
            return false;
        }

        for (LinkSpec link: links) {//循环LinkSpec对象
            applyLink(link.url, link.start, link.end, text);//设置显示样式
        }

        return true;
    }

其中gatherLinks方法,根据正则表达式收集链接地址,组建LinkSpec对象集合,源码如下:

private static final void gatherLinks(ArrayList<LinkSpec> links,
           Spannable s, Pattern pattern, String[] schemes,
           MatchFilter matchFilter, TransformFilter transformFilter) {
       Matcher m = pattern.matcher(s);

       while (m.find()) {
           int start = m.start();
           int end = m.end();

           if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
               LinkSpec spec = new LinkSpec();
               String url = makeUrl(m.group(0), schemes, m, transformFilter);

               spec.url = url;
               spec.start = start;
               spec.end = end;

               links.add(spec);
           }
       }
   }

其中applyLink方法,设置显示样式,源码如下:

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);//设置显示样式
   }

这个时候觉得豁然开朗,可以明白autoLink的核心就是通过正则表达式收集链接地址,构造LinkSpec对象集合,通过调用text.setSpan方法设置样式显示。
那链接地址的点击事件默认用浏览器打开,是在哪里处理的呢?
在上面applyLink方法中有URLSpan,我们查看下URLSpan源码,如下:

public class URLSpan extends ClickableSpan implements ParcelableSpan {

  private final String mURL;

  /**
   * Constructs a {@link URLSpan} from a url string.
   *
   * @param url the url string
   */
  public URLSpan(String url) {
      mURL = url;
  }

  /**
   * Constructs a {@link URLSpan} from a parcel.
   */
  public URLSpan(@NonNull Parcel src) {
      mURL = src.readString();
  }

  @Override
  public int getSpanTypeId() {
      return getSpanTypeIdInternal();
  }

  /** @hide */
  @Override
  public int getSpanTypeIdInternal() {
      return TextUtils.URL_SPAN;
  }

  @Override
  public int describeContents() {
      return 0;
  }

  @Override
  public void writeToParcel(@NonNull Parcel dest, int flags) {
      writeToParcelInternal(dest, flags);
  }

  /** @hide */
  @Override
  public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
      dest.writeString(mURL);
  }

  /**
   * Get the url string for this span.
   *
   * @return the url string.
   */
  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());
      try {
          context.startActivity(intent);
      } catch (ActivityNotFoundException e) {
          Log.w("URLSpan", "Actvity was not found for intent, " + intent.toString());
      }
  }
}

可以看到URLSpan继承了ClickableSpan,在onClick方法中处理点击事情,所以如果需要修改链接的点击事件,我们需要继承ClickableSpan或是URLSpan,重写onClick事件。

说明

在上面方法中是直接调用一个方法interceptHyperLink来实现的拦截链接地址,修改链接地址的点击事件,有时候会觉得很麻烦,在想能不能通过自定义view继承TextView的方式实现这种效果呢,答案是肯定可以啊,原理都是一样的,具体可以查看这篇文章
注明:关于TextView的autoLink属性设置超链接的完整demo请点击这里

参考博客

https://blog.csdn.net/zhangjinhuang/article/details/52416608
https://blog.csdn.net/sahadev_/article/details/53639168
https://blog.csdn.net/ziyiwangchen/article/details/51861222

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值