Android富文本编辑器(四):HTML文本转换

在真实的工程开发中,一个富文本编辑器,不是仅仅可以编辑显示,还需要处理富文本的转换与解析,方便传输与存储。一般来说,HTML文本是比较理想的网络传输格式。如何将安卓的span式富文本和HTML文本之间进行互转,是本篇介绍的重点。
首先,介绍富文本转成HTML。我们需要做的,是遍历文本中的span对象,并用合适的html标签来修饰span对象对应的文本。示例代码如下:

public static String convertSpannedToRichText(Spanned spanned) {
    List<CharacterStyle> spanList =
            Arrays.asList(spanned.getSpans(0, spanned.length(), CharacterStyle.class));
    SpannableStringBuilder stringBuilder = new SpannableStringBuilder(spanned);
    for (CharacterStyle characterStyle : spanList) {
        int start = stringBuilder.getSpanStart(characterStyle);
        int end = stringBuilder.getSpanEnd(characterStyle);
        if (start >= 0) {
            String htmlStyle = handleCharacterStyle(characterStyle,
                    stringBuilder.subSequence(start, end).toString());
            if (htmlStyle != null) {
                stringBuilder.replace(start, end, htmlStyle);
            }
        }
    }
    return stringBuilder.toString();
}

private static String handleCharacterStyle(CharacterStyle characterStyle, String text) {
    if (characterStyle instanceof BoldSpan) {
        return String.format("<b>%s</b>", text);
    } else if (characterStyle instanceof UrlSpan) {
        UrlSpan span = (UrlSpan) characterStyle;
        return String.format("<a href=\"%s\">%s</a>", span.getValue(), text);
    } else if (characterStyle instanceof EmojiSpan) {
        EmojiSpan span = (EmojiSpan) characterStyle;
        return String.format("<img src=\"%s\" alt=\"[%s]\" class=\"yiqiFace\"/>",
                span.getUrl(), span.getName());
    } else if (characterStyle instanceof FakeImageSpan) {
        FakeImageSpan span = (FakeImageSpan) characterStyle;
        return String.format("<img src=\"%s\" />", span.getValue());
    } if (characterStyle instanceof ImageSpan) {
        ImageSpan span = (ImageSpan) characterStyle;
        return String.format("<img src=\"%s\" />", TextUtils.isEmpty(span.getUrl()) ?
                span.getFilePath() : span.getUrl());
    }
    return null;
}

注:上述代码在处理一些复杂的span嵌套情况时,可能会有问题。如果要考虑到span嵌套的情况,可能需要全新的思路和写法。后续如果有改进会再更新。
下面介绍如何将HTML转化为安卓富文本。这里使用了一个开源库:TagSoup,来处理Html内容的解析。TagSoup是一个解析HTML的java开源库,一般用作HTML的正则化。TagSoup的介绍可以参考网上其它文章,这里不再赘述。而我们要做的,是实现自己的SAX内容处理器,实现org.xml.sax.ContentHandler接口,并set到TagSoupParser中。具体代码都在:RichTextConvertor这个类中。下面节选几个比较重要的方法:

@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
    handleStartTag(localName, atts);
}

@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
    handleEndTag(localName);
}

@Override
public void characters(char[] ch, int start, int length) throws SAXException {
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < length; i++) {
        char c = ch[i + start];
        sb.append(c);
    }

    mResult.append(sb);
}

// ****************************************** Handle Tags *******************************************

private void handleStartTag(String tag, Attributes attributes) {
    if (tag.equalsIgnoreCase("a")) {
        startAHref(attributes);
    } else if (tag.equalsIgnoreCase("img")) {
        startImg(attributes);
    } else if (tag.equalsIgnoreCase("b") || tag.equalsIgnoreCase("strong")) {
        start(new Bold());
    }
}

private void handleEndTag(String tag) {
    if (tag.equalsIgnoreCase("a")) {
        endAHref();
    } else if (tag.equalsIgnoreCase("b")|| tag.equalsIgnoreCase("strong")) {
        end(Bold.class, new BoldSpan());
    }
}

private void startAHref(Attributes attributes) {
    String href = attributes.getValue("", "href");
    int len = mResult.length();
    mResult.setSpan(new Href(href), len, len, Spanned.SPAN_MARK_MARK);
}

private void start(Object mark) {
    int len = mResult.length();
    mResult.setSpan(mark, len, len, Spanned.SPAN_MARK_MARK);
}

private void endAHref() {
    int len = mResult.length();
    Object obj = getLast(Href.class);
    int where = mResult.getSpanStart(obj);

    mResult.removeSpan(obj);

    if (where != len) {
        Href h = (Href) obj;
        if (h.mHref != null) {
            mResult.setSpan(new UrlSpan(h.mHref),
                    where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
}

private void end(Class<? extends Object> kind, Object repl) {
    int len = mResult.length();
    Object obj = getLast(kind);
    int where = mResult.getSpanStart(obj);

    mResult.removeSpan(obj);

    if (where != len) {
        // Note: use SPAN_EXCLUSIVE_EXCLUSIVE, the TemporarySpan will be replaced by a SPAN_EXCLUSIVE_INCLUSIVE span
        mResult.setSpan(new TemporarySpan(repl), where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
}

private Object getLast(Class<? extends Object> kind) {
    /*
     * This knows that the last returned object from getSpans()
     * will be the most recently added.
     */
    Object[] objs = mResult.getSpans(0, mResult.length(), kind);
    return objs.length == 0 ? null : objs[objs.length - 1];
}

private void startImg(Attributes attributes) {
    int len = mResult.length();
    String alt = attributes.getValue("", "alt");
    String src = attributes.getValue("", "src");
    String classString = attributes.getValue("", "class");

    // Unicode Character 'OBJECT REPLACEMENT CHARACTER' (U+FFFC)
    // see http://www.fileformat.info/info/unicode/char/fffc/index.htm
    mResult.append("\uFFFC");
    FakeImageSpan imageSpan = new FakeImageSpan(src);
    mResult.setSpan(imageSpan, len, len + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

private static class Bold {
}

这里需要分三种情况进行讨论:

  1. 对于img这种标签,我们取出其中的属性,就可以进行替换(有的读者可能发现,在img标签的处理上,我使用了一个叫做FakeImageSpan的类。这时因为在传输、存储过程中,图片都是使用了url远程路径或者本地路径,需要先进行下载或加载,才可以使用真正的ImageSpan进行替换);
  2. 对于href这种标签,需要先读取内容,直到出现截止标签,再用真正的URL内容替换掉占位span;
  3. 对于b和strong这样的标签,先用占位span占位后,在替换成TemporarySpan,并采用SPAN_EXCLUSIVE_EXCLUSIVE格式。在后面处理完所有输入后,统一替换成相应得span,并采取SPAN_EXCLUSIVE_INCLUSIVE格式,这样后续的输入都会采用相同的格式。具体的处理请参考RichTextConvertor
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容