欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android使用内置WebView打开TextView超链接的实现方法

程序员文章站 2023-12-14 11:31:34
需求原因 最近工作中遇到一个需求,后来通过查找相关的资料终于解决了,索性记录下来分享给大家,需要的朋友们可以参考学习。 该需求如下: **产品说,我们要实现问答功能,...

需求原因

最近工作中遇到一个需求,后来通过查找相关的资料终于解决了,索性记录下来分享给大家,需要的朋友们可以参考学习。

该需求如下:

**产品说,我们要实现问答功能,答案内的链接要使用内置的浏览器打开。

**视觉说,我们要给超链接标上我们自己的颜色。

如图:

Android使用内置WebView打开TextView超链接的实现方法

下面我们分析下如何实现。

使用html

常规方法,给定一段标准html文档,使用html.fromhtml()封装,直接使用textview显示。

textview textview = (textview) findviewbyid(r.id.detailed_question_tv_answer);
string teststring =
"亲,一般遇到这问题您可以这样哦:<br>1.可以<font color='#ff8785'><a href='http://m.kaola.com'>催发货</a></font>哦~<br>2.然后耐心等待哦~<br>3.1-3天后新也可以拨打我们的客服.";
textview.setmovementmethod(linkmovementmethod.getinstance());
// 设置链接颜色
textview.setlinktextcolor(getresources().getcolor(r.color.red_ff8785));
spanned htmlstring = html.fromhtml(teststring);
textview.settext(htmlstring);

使用常规方法无论怎么设置,链接都会使用隐式intent打开,即使用外部的浏览器打开,不符合咱们产品的需求呀。怎么才能监听这个使用并使用内部webview打开呢?使用spannablestringbuilder。

使用spannablestringbuilder

直接上代码。

textview textview = (textview) findviewbyid(r.id.detailed_question_tv_answer);
string teststring =
 "亲,一般遇到这问题您可以这样哦:<br>1.可以<font color='#ff8785'><a href='http://m.kaola.com'>催发货</a></font>哦~<br>2.然后耐心等待哦~<br>3.1-3天后新也可以拨打我们的客服.";
textview.setmovementmethod(linkmovementmethod.getinstance());
textview.setlinktextcolor(getresources().getcolor(r.color.red_ff8785));
string linktext = "催发货";
int startindexoflink = teststring.indexof(linktext);
int endindexoflink = startindexoflink + linktext.length();
spannablestringbuilder spannablestringbuilder = new spannablestringbuilder(teststring);
spannablestringbuilder.setspan(new clickablespan() {
 @override
 public void onclick(view widget) {
  activityutils.startwebviewactivity(detailedquestionactivity.this, "http://m.kaola.com", false);
 }
}, startindexoflink, endindexoflink, spannable.span_inclusive_exclusive);
textview.settext(spannablestringbuilder);

当然,这个方法是有很大的局限性的,必须知道链接在文案中的具体位置,以及链接的地址才能够使用这种方法。按照这种思路,我们必须使用正则表达式获取对应的a标签才能得到链接。这种方法拿到的链接在文案中的具体位置是难以把握的,很有可能出错。

html + spannablestringbuilder

有没有第三种方法,即能够解析到给定文案中的所有html标签,又能够使用内置的webview打开这个链接?从第一种方法中,我们直接使用html.fromhtml()方法拿到对应的spanned结果,我们可以从这里入手,看看这个方法是怎么解析html标签的

public static spanned fromhtml(string source, imagegetter imagegetter,
        taghandler taghandler) {
 // 使用org.ccil.cowan.tagsoup.parser作为解析器       
 parser parser = new parser();
 try {
  parser.setproperty(parser.schemaproperty, htmlparser.schema);
 } catch (org.xml.sax.saxnotrecognizedexception e) {
  // should not happen.
  throw new runtimeexception(e);
 } catch (org.xml.sax.saxnotsupportedexception e) {
  // should not happen.
  throw new runtimeexception(e);
 }
 // 使用htmltospannedconverter将ttml转换成spanned
 htmltospannedconverter converter =
   new htmltospannedconverter(source, imagegetter, taghandler,
     parser);
 return converter.convert();
}

接下来看一下htmltospannedconverter.convert()这个方法。htmltospannedconverter实现了contenthandler接口,contenthandler用于处理xml文档的解析细节。

public spanned convert() {

 mreader.setcontenthandler(this);
 try {
  mreader.parse(new inputsource(new stringreader(msource)));
 } catch (ioexception e) {
  // we are reading from a string. there should not be io problems.
  throw new runtimeexception(e);
 } catch (saxexception e) {
  // tagsoup doesn't throw parse exceptions.
  throw new runtimeexception(e);
 }

 // fix flags and range for paragraph-type markup.
 object[] obj = mspannablestringbuilder.getspans(0, mspannablestringbuilder.length(), paragraphstyle.class);
 for (int i = 0; i < obj.length; i++) {
  int start = mspannablestringbuilder.getspanstart(obj[i]);
  int end = mspannablestringbuilder.getspanend(obj[i]);

  // if the last line of the range is blank, back off by one.
  if (end - 2 >= 0) {
   if (mspannablestringbuilder.charat(end - 1) == '\n' &&
    mspannablestringbuilder.charat(end - 2) == '\n') {
    end--;
   }
  }

  if (end == start) {
   mspannablestringbuilder.removespan(obj[i]);
  } else {
   mspannablestringbuilder.setspan(obj[i], start, end, spannable.span_paragraph);
  }
 }

 return mspannablestringbuilder;
}

我们关心html是如何被转换成spanned就够了。在整个解析html的过程中,是通过spannablestringbuilder.setspan(object what, int start, int end, int flags)这个方法不断进行html->spanned转换的。例如,遇到一个a标签,则会通过下边的方法设置一个span,在spannabblestringbuilder内部,span用一个数组表示,是可以累加的。

// 遇到a标签头
private static void starta(spannablestringbuilder text, attributes attributes) {
 string href = attributes.getvalue("", "href");

 int len = text.length();
 text.setspan(new href(href), len, len, spannable.span_mark_mark);
}
// a标签结束
private static void enda(spannablestringbuilder text) {
 int len = text.length();
 object obj = getlast(text, href.class);
 int where = text.getspanstart(obj);

 text.removespan(obj);

 if (where != len) {
  href h = (href) obj;

  if (h.mhref != null) {
   text.setspan(new urlspan(h.mhref), where, len,
       spannable.span_exclusive_exclusive);
  }
 }
}

可以看到a标签的转换方法,实际上,a标签最后被转换成了一个urlspan。

看到这里,思路就来了!实际上,html.fromhtml()方法最后转换成的对象是一个spannablestringbuilder,我们可以拿到这个对象的引用,然后获取所有的urlspan,最后把这些urlspan全部转换成可以监听的事件不就实现了吗?实际上并没有这么简单,urlspan是一个类,继承自clickablespan,覆盖了其中的onclick(view)方法:

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);
 }
}

这里已经默认使用了隐式intent的方式打开uri。我们不能直接改变urlspan类的实现方式,但可以继承这个类并覆盖掉它的onclick(view)方法,或者直接继承clickablespan。但是,这样还是会有问题,原先的urlspan早就在解析的时候存在于spannablestringbuilder中的,我们需要先移除对应的urlspan,然后再设置自己实现的新的clickablespan就可以了。

具体代码如下:

public static spannablestringbuilder settextlinkopenbywebview(final context context, string answerstring) {
 if (!textutils.isempty(answerstring)) {
  spanned htmlstring = html.fromhtml(answerstring);
  if (htmlstring instanceof spannablestringbuilder) {
   spannablestringbuilder spannablestringbuilder = (spannablestringbuilder) htmlstring;
   // 取得与a标签相关的span
   object[] objs = spannablestringbuilder.getspans(0, spannablestringbuilder.length(), urlspan.class);
   if (null != objs && objs.length != 0) {
    for (object obj : objs) {
     int start = spannablestringbuilder.getspanstart(obj);
     int end = spannablestringbuilder.getspanend(obj);
     if (obj instanceof urlspan) {
      //先移除这个span,再新添加一个自己实现的span。
      urlspan span = (urlspan) obj;
      final string url = span.geturl();
      spannablestringbuilder.removespan(obj);
      spannablestringbuilder.setspan(new clickablespan() {
       @override
       public void onclick(view widget) {
        activityutils.startwebviewactivity(context, url, true);
       }
      }, start, end, spanned.span_inclusive_exclusive);
     }
    }
   }
   return spannablestringbuilder;
  }
 }
 return new spannablestringbuilder(answerstring);
}

总结

textview真的是android里最强大的组件之一,复杂度很高,源码甚至比activity还要多。正确的使用textview具有意想不到的效果~文中为了解决textview组件中的超链接使用app自带的webview打开这个问题进行了一次探讨,最终通过hook拿到urlspan,移除它并实现自己的clickablespan,最终解决问题。好了,以上就是这篇文章的全部内容了,希望本文的内容对各位android开发们能带来一定的帮助,如果有疑问大家可以留言交流。

上一篇:

下一篇: