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>标签添加了自定义属性,hrefshow_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()是系统提供的方法,而HtmlParserLinkHandler使我们实现的类。

解析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

原创粉丝点击