Span使用之利用自定义Span解析Html中特殊标签实现类似微博@效果
来源:互联网 发布:淘宝买小饰品文艺店铺 编辑:程序博客网 时间:2024/05/21 11:53
Span使用之利用自定义Span解析Html中特殊标签实现类似微博@效果
在前两篇博客中,讲解了系统已经定义好的Span
,并且怎么利用系统的span
实现一些特殊的效果。本篇博客将是这一系列的最后一篇。
- Span使用之系统提供的Span基本样式
- Span使用之利用系统Span样式实现模糊搜索,匹配变色的特殊UI效果
- Span使用之利用自定义Span解析Html中特殊标签实现类似微博@效果
实现效果
分析一下实现效果,就是一长串文字,部分文字根据我们的要求变色,并且可以点击。点击的内容有我们定义。
本篇博客的实现和系统的UrlSpan
实现类似。
而对于微博的@
的效果,和该实现流程几完全一致。看完你就明白了。
原理分析
系统提供的Span
中有一个ClickableSpan
,不过他是一个抽象类,需要我们实现onClick
方法,并且他也提供了修改颜色方法,我们只需要实现这个类即可。并且通过第一篇博客中的讲解,将他设置到对应文本索引上就可以了。
如上所述确实可以实现,不过对于一个文本,如果有多个地方,那么需要我们设置多个ClickableSpan
,这很不利于使用。假如实现微博的@
功能,多个@
并且数据可能是后台返回,如果我们一一的拼接,并没有提供多大的便利。
这时候我们需要用到一些其他方面的知识。关于TextView
我们可以放入一个Html
格式的文本,他会自动解析,而本章的关键便在于HTML
,我们自定义一个标签,类似于<a>
标签一样,让标签中的内容可以点击。
根据如上所述,整个流程实现如下:
- 定义自定义标签和编写HTML
- 设置HTML到
TextView
上。 - 解析
Html
文本并获取到自定义标签。 - 自定义类实现
ClickableSpan
。 - 将解析的自定义标签中的内容设置自定义的
Span
实现
定义自定义标签和编写HTML
首先看一下编写的HTML
文本
我已阅读并同意<app_a href="https://www.baidu.com" show_underline=false >《注册协议》</app_a>、<app_a href="https://www.baidu.com" show_underline=false >《用户服务协议》</app_a>
在这里,我们定义<app_a>
标签作为特殊标签,当解析到该标签的时候表示其要作为特殊处理,设置点击事。
同时对于<app_a>
标签添加了自定义属性,href
和show_underline
。
将这一段文本写到string.xml
文件中,因为Html
中也会有标签,可能会和xml
冲突,所以我们需要添上相关的标识。
<string name="tag_html"> <![CDATA[ 我已阅读并同意<app_a href="https://www.baidu.com" show_underline=false >《注册协议》</app_a>、<app_a href="https://www.baidu.com" show_underline=false >《用户服务协议》</app_a> ]]> </string>
设置HTML到TextView
上。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mText = ((TextView) findViewById(R.id.text)); // 使点击实现可以传递到我们定义的`span`上 mText.setMovementMethod(LinkMovementMethod.getInstance()); // 设置文本 mText.setText(fromHtml(getString(R.string.tag_html))); } // 解析html public static Spanned fromHtml(String html) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, null, new HtmlParser(new LinkHandler())); } else { return Html.fromHtml(html, null, new HtmlParser(new LinkHandler())); } }
关键方法在于Html.from()
,该方法能够实现将html
转化为spanned
。同时他能够传入一个解析器,实现自定义解析。因为要解析自定义标签,所以我们要传入一个自定义解析器。
Html.from()
是系统提供的方法,而HtmlParser
,LinkHandler
使我们实现的类。
解析Html
文本并获取到自定义标签
看一下HtmlParser
的实现
public class HtmlParser implements Html.TagHandler, ContentHandler { // ...}
实现了两个接口,其中tagHandler
是实现解析器必须实现的接口,他其中定义了一个方法
public interface TagHandler { void handleTag(boolean opening, String tag, Editable output, XMLReader attributes); }
在解析的过程中,每遇到一个标签,都会回调这个方法,对于我们现在的文本,他的回调如下:
03-20 14:24:26.392 9727-9727/com.spearbothy.htmlparser I/info: open:truetag:htmloutput: * 03-20 14:24:26.392 9727-9727/com.spearbothy.htmlparser I/info: open:truetag:bodyoutput: * 03-20 14:24:26.392 9727-9727/com.spearbothy.htmlparser I/info: open:truetag:app_aoutput:我已阅读并同意 * 03-20 14:24:26.392 9727-9727/com.spearbothy.htmlparser I/info: open:falsetag:app_aoutput:我已阅读并同意《注册协议》 * 03-20 14:24:26.392 9727-9727/com.spearbothy.htmlparser I/info: open:truetag:app_aoutput:我已阅读并同意《注册协议》、 * 03-20 14:24:26.392 9727-9727/com.spearbothy.htmlparser I/info: open:falsetag:app_aoutput:我已阅读并同意《注册协议》、《用户服务协议》 * 03-20 14:24:26.392 9727-9727/com.spearbothy.htmlparser I/info: open:falsetag:bodyoutput:我已阅读并同意《注册协议》、《用户服务协议》 * 03-20 14:24:26.392 9727-9727/com.spearbothy.htmlparser I/info: open:falsetag:htmloutput:我已阅读并同意《注册协议》、《用户服务协议》
再看第二个接口,他是一个解析处理类,因为对于TagHandler
的回调,,没有明显的区分开闭标签,都在一个方法中回调,而ContentHandler
能够区分开闭标签进行回调。他里面有两个最重要的方法
public void startElement(String uri, String localName, String qName, Attributes atts) public void endElement(String uri, String localName, String qName)
那么看一下流程开始handleTag()
的实现。并且一些相关字段:
// 处理我们自定义标签的类 private final TagHandler mHandler; // 系统的解析器 private ContentHandler mWrapperContentHandler; // 解析的文本那内容 private Editable mOutput; // 保存我们解析标签的状态 private ArrayDeque<Boolean> mTagStatus = new ArrayDeque<>(); @Override public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { if (mWrapperContentHandler == null) { mOutput = output; // 保存系统的解析器处理 mWrapperContentHandler = xmlReader.getContentHandler(); // 设置当前类处理系统标签 xmlReader.setContentHandler(this); // 记录当前标签是否处理 mTagStatus.addLast(Boolean.FALSE); } }
整个方法的流程如下:
- 保存文本的输出,以便获取文本
- 获取系统的默认解析文本的处理器,放入我们自己的标签处理器,就是当前类,该类实现了
ContentHandler
- 保存当前标签是否处理。true表示当前是自定义标签。false不是当前自定义标签。
在这里有必要说一下mTagStatus
,他是一个队列,因为标签都是一一对应,有一个开就有一个闭,那么我们在标签开始的时候添加是否处理,在标签结束的时候获取这个索引,就只到是否需要处理了。
然后看一下实现ContentHandler
中的方法的处理流程
@Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { Log.i("info", "start:--" + "uri:" + uri + " localName:" + localName + " qName:" + qName); // 判断当前是否是需要处理的自定义标签的类 boolean isHandled = mHandler.handleTag(true, localName, mOutput, atts); mTagStatus.addLast(isHandled); // 如果不是,交由系统处理 if (!isHandled) mWrapperContentHandler.startElement(uri, localName, qName, atts); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { Log.i("info", "end:--" + "uri:" + uri + " localName:" + localName + " qName:" + qName); // 获取状态,判断是否自定义需要处理 if (!mTagStatus.removeLast()) { mWrapperContentHandler.endElement(uri, localName, qName); } else { mHandler.handleTag(false, localName, mOutput, null); } }
在这里有必要强调一点,对于开闭标签,我们的逻辑应该是这样的:在标签开始时,打上标记,在标签结束时,处理相关逻辑,因为只有在结束标签,我们才能知道文本的长短。
在代码中有一个mHandler
,该类是我们自己编写的类,如果你有印象的话,在解析Html
时,我们传入了一个Handler
// LinkHandlerHtml.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, null, new HtmlParser(new LinkHandler()));
我们在HtmlParser
中定义一个接口,然后具体的自定义逻辑交给LinkHandler
实现。
public class HtmlParser implements Html.TagHandler, ContentHandler { public interface TagHandler { boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes); }}
此时一定要将我们自定义的
TagHandler
和系统的区分开
看一下LinkHandler
的实现
public class LinkHandler implements HtmlParser.TagHandler { @Override public boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes) { if (tag.equalsIgnoreCase("app_a")) { if (opening) { // 开始标签,获取对应值 String href = attributes.getValue("href"); String showUnderline = attributes.getValue("show_underline"); if (TextUtils.isEmpty(showUnderline)) { showUnderline = "true"; } // 构造标签实体,用以保存数据 LinkTagAttribute entity = new LinkTagAttribute(); entity.setHref(href); entity.setShowUnderline(Boolean.parseBoolean(showUnderline)); // 将解析的数据实体暂时保存到文本上 (打标记) output.setSpan(entity, output.length(), output.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } else { // 获取之前保存的标记 LinkTagAttribute entity = getLast(output, LinkTagAttribute.class); if (entity != null) { // 获取开始标签的位置索引 int start = output.getSpanStart(entity); // 移除之前的标记 output.removeSpan(entity); int end = output.length(); if (start != end){ // 设置自定义的Span output.setSpan(new AppUrlSpan(entity),start,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } return true; } return false; }}
该处理的方法中分为两部分,开始标签和闭合标签。
开始标签的时候,获取到标签以及其自定义属性保存到实体中,同时Editable
提供将实体作为标记打入文本上,类似于View.setTag()
方法一样。
然后在闭合标签的时候,获取之前打的标记,然后根据标记的内容设置生成Span
并设置。
看一下自定义属性实体LinkTagAttribute
public class LinkTagAttribute implements Parcelable { private String href; private boolean isShowUnderline; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.href); dest.writeByte(isShowUnderline ? (byte) 1 : (byte) 0); } protected LinkTagAttribute(Parcel in) { this.href = in.readString(); this.isShowUnderline = in.readByte() != 0; } public static final Creator<LinkTagAttribute> CREATOR = new Creator<LinkTagAttribute>() { @Override public LinkTagAttribute createFromParcel(Parcel source) { return new LinkTagAttribute(source); } @Override public LinkTagAttribute[] newArray(int size) { return new LinkTagAttribute[0]; } }; public LinkTagAttribute() { } //.....}
自定义类实现ClickableSpan
该类的实现就比较简单了
public class AppUrlSpan extends ClickableSpan implements ParcelableSpan { private static final int APP_URL_SPAN = 100000; private LinkTagAttribute entity; // .... @Override public void onClick(View widget) { // 点击事件,简单的弹出提示 Log.i("info", "click"); Toast.makeText(widget.getContext(), entity.getHref() + "", Toast.LENGTH_SHORT).show(); } @Override public int getSpanTypeId() { // 类型标识 return APP_URL_SPAN; } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); // 修改文本的状态 ds.bgColor = Color.TRANSPARENT; ds.setUnderlineText(entity.isShowUnderline()); }}
将解析的自定义标签中的内容设置自定义的Span
最后一步,该步骤的代码在第三步的时候就已经贴出。
在解析到结束标签是的代码如下
// 获取之前保存的标记 LinkTagAttribute entity = getLast(output, LinkTagAttribute.class); if (entity != null) { // 获取开始标签的位置索引 int start = output.getSpanStart(entity); // 移除之前的标记 output.removeSpan(entity); int end = output.length(); if (start != end){ // 设置自定义的Span output.setSpan(new AppUrlSpan(entity),start,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } }
获取标记,打上Span
。
源码地址
具体的源码细节已经上传到github上,欢迎访问https://github.com/AlexSmille/HtmlParser
- Span使用之利用自定义Span解析Html中特殊标签实现类似微博@效果
- Span使用之利用系统Span样式实现模糊搜索,匹配变色的特殊UI效果
- html中<span>标签
- HTML <span> 标签简介
- html-<span>标签
- HTML <span> 标签 (w3school)
- html span标签详解
- HTML <span> 标签
- HTML <span> 标签
- (18)HTML标签详解之<div> <span>
- html中Span标签的点击事件
- 使用Span实现各种酷炫效果
- HTML <span> 标签
- html标签div和span
- html中的<span>标签用法
- html中span标签中width属性无效的解决方法
- HTML中Div、span、label标签的区别
- HTML中div和span两个标签的区别
- 树莓派IP地址设置
- 【大数据部落】NBA体育决策和数据挖掘分析
- eclipse如何导入okhttp 2.x源码
- Ubuntu中安装中文输入法
- eclipse tomcat May be locked by another process 解决
- Span使用之利用自定义Span解析Html中特殊标签实现类似微博@效果
- Java 基础的东西
- 刷题——SortList
- Windows下多Tomcat实例
- @RequestMapping 用法详解之地址映射
- 【bzoj3450】 Tyvj1952 Easy
- 2809: [Apio2012]dispatching
- 程序是在何种环境下运行的
- Spark Streaming 调优指南