Oct 15

Android:fromHtml方法踩坑

Lrdcq , 2015/10/15 23:01 , 程序 , 閱讀(6480) , Via 本站原創
学习使用fromhtml方法来设置比较花哨的textview样式踩坑实录。
首先,得出的结论是:复杂的文本样式,并不推荐使用formhtml来实现,退一步自己手动设定span内容和范围,进一步使用webview来渲染。不是必须一定不要用fromhtml!





1.fromhtml兼容性

首先,fromhtml支持的标签是有限的,支持的标签包括:(参看HTML.java中的私有方法handleStartTag)
<a href="..."> 定义链接内容
<b> 定义粗体文字 b 是blod的缩写
<big> 定义大字体的文字
<blockquote> 引用块标签 属性包括:Common -- 一般属性 cite -- 被引用内容的URI
<br> 定义换行
<cite> 表示引用的URI
<dfn> 定义标签 dfn 是defining instance的缩写
<div align="...">
<em> 强调标签 em 是emphasis的缩写
<font size="..." color="..." face="...">
<h1>
<h2>
<h3>
<h4>
<h5>
<h6>
<i> 定义斜体文字
<img src="...">
<p> 段落标签,里面可以加入文字,列表,表格等
<small> 定义小字体的文字
<strike> 定义删除线样式的文字 不符合标准网页设计的理念,不赞成使用. strike是strikethrough的缩写
<strong> 重点强调标签
<sub> 下标标签 sub 是subscript的缩写
<sup> 上标标签 sup 是superscript的缩写
<tt> 定义monospaced字体的文字 不赞成使用. 此标签对中文没意义 tt是teletype or monospaced text style的意思
<u> 定义带有下划线的文字 u是underlined text style的意思

然而令人沮丧的是,就算官方说明支持的标签,支持性也是大打折扣,部分旧版中<a>超链接几乎是无法点击的,<strong>,<small><big>等标签也会出现不能显示的问题。
更糟糕的是,根据网络上的问题反馈,一些标签的解释在不同配置不同安卓版本下渲染出的样式是不同的,这直接导致不敢用->不能用。

2.图片加载

遇到img标签的时候,图片并不会加载我们看到fromhtml的第二种写法是 fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler)就明白了,我们可以传入一个ImageGetter进去。
ImageGetter是一个接口,我们实现一次这个接口,出来就看到这个:
private Html.ImageGetter imageGetter=new Html.ImageGetter(){
  @Override
  public Drawable getDrawable(String source) {
    return null;
  }
};

主要是要实现getDrawable方法,入参是img标签中src属性的字符串,而返回的是这个图片的drawable。作为在主线程中加载图片的方法,它是同步的...同步的...步的...的...
这简直是噩梦,难道我们要在ui线程中加载一个图片,如果做成异步的需要用比较取巧的方法来返回资源了。
解决这个方法的思路是把异步获取drawable这个对象holder住,当异步调用完成后手动刷新textview来渲染出图片。新建如下类:
private class BitmapDrawablePlaceHolder extends BitmapDrawable {
  protected Drawable drawable;
  @Override
  public void draw(final Canvas canvas) {
    if (drawable != null) {
      drawable.draw(canvas);
    }
  }
  public void setDrawable(Drawable drawable) {
    this.drawable = drawable;
  }
}

那么先把这个占位符直接返回给textview,之后set回去,再刷新界面就可以完成了,ImageGetter的核心逻辑是:
@Override
public Drawable getDrawable(final String source) {
  final BitmapDrawablePlaceHolder result = new BitmapDrawablePlaceHolder();
  new AsyncTask<Void, Void, Bitmap>() {
    @Override
    protected Bitmap doInBackground(final Void... meh) {
      try {
        return pablo.load(source).get();
      } catch (Exception e) {
        return null;
      }
    }
    @Override
    protected void onPostExecute(final Bitmap bitmap) {
       try {
          final BitmapDrawable drawable = new BitmapDrawable(resources, bitmap);
          drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
          result.setDrawable(drawable);
          result.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
          textView.setText(textView.getText());
       } catch (Exception e) {
       }
     }
  }.execute((Void) null);
  return result;
}

其中我们需要保留textview的引用来刷新textview,而getDrawable方法是无法告诉我到底是哪个textview触发的,因此textview对象需要由ImageGetter实例储存,因此一个ImageGetter实例只能对应一个textview。这是一个值得商榷之处。
还要一个问题就是每一次图片加载完成都会完全刷新一个textview,这对于长textview或者图片很多的textview(比如新闻类app,微博类app)是不可以接受的,这也是值得商榷之处。
这两处疑问点的修改方式已经超越了textview渲染的范畴,需要涉及到span预渲染的操作,这个在此不议了。

3.更多标签

我们知道,fromhtml中的各种文字样式大多都是用span来实现的。span接口下的预定义方法千千万,而fromhtml能解析的只有这么十来个,我们当然在想是否可以自定义标签。
是可以的,在fromhtml无法识别的标签会尝试发到TagHandler中去,那就是fromhtml高级用法的第三个对象。(也就是说想重写fromhtml能识别的标签还是得重写fromhtml)
这个接口实现如下:
private Html.TagHandler tagHandler =new Html.TagHandler() {
    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
    }
};

显然核心方法也只有一个handleTag。handleTag有四个参数,第二个是tag名字,第三个是整个output的引用,我们编辑样式直接在output上操作就是,第四个是tag在xml的对象,可以用它来读取tag中的属性。那第一个opening是什么鬼?opening表示的是遇到的是tag的开还闭,也就是遇到的是tag的开始还是结束(封闭的tag就是一个开始,并没有结束),也就是说在实际处理过程中,如果是开的话,我们要记录开tag所在的位置,如过是闭的话,我们要找到开的时候的位置,然后为中间这一段设定上我们要设定的样式。基本的运行逻辑就是这样。
所以我们具体代码中是怎么分发的呢。首先我们用一堆if把不同的标签分发给不同的处理方法,类似于如下:
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
    if (tag.equalsIgnoreCase("strike") || tag.equals("s")) {
        processStrike(opening, output);
   } else if (tag.equalsIgnoreCase("center")) {
        processCenter(opening, output);
   } else if (tag.equalsIgnoreCase("right")) {
        processRight(opening, output);
   }
}

对于每一个处理方法我们这样:
private void processCenter(boolean opening, Editable output) {
    int len = output.length();
    if (opening) {
        output.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), len, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    } else {
        Object obj = getLast(output, AlignmentSpan.Standard.class, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        int where = output.getSpanStart(obj);
        output.removeSpan(obj);
        if (where < 0) where = 0;
        if (where != len) {
            output.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
}

首先我们判断是开还是闭标签,开的话就在当前output的偏移量位置设置上样式。这样同时可以处理单tag的情况。
如果遇到闭tag时,我们首先找到之前开tag的最后一个tag的位置,使用如下方法:
private Object getLast(Editable text, Class kind, int mark) {
  Object[] objs = text.getSpans(0, text.length(), kind);
  if (objs.length == 0) {
        return null;
  } else {
        for (int i = objs.length; i > 0; i--) {
            if (text.getSpanFlags(objs[i - 1]) == mark) {
                return objs[i - 1];
             }
        }
        return null;
  }
}

很明显这是一个精确有效的打tag并寻找的方法。我们找到上一个开tag就可以确定这个tag的封闭区间,同样给这个区间设置上相同的样式就可以了。
然而事情并没有那么简单,经常遇到的是各种各样的样式无效甚至崩溃。最常见的一个奔溃是这个:PARAGRAPH span must end at paragraph boundary。
从软解决的方法上来讲,这句话是要求这个定义为段落的标签以上一个是回车符开始,所以说在报错的地方加一个\n就可以解决。不过很多情况下这其实是不满足我们的需求的,这个报错核心的问题在于使用的span是实现的ParagraphStyle接口,被判定为段落级样式了。
logo