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开发们能带来一定的帮助,如果有疑问大家可以留言交流。