传统上TextView一般都是比较简单的使用,展示一下文字,文字的字体、颜色等也是直接在xml里设置。这种使用对大家来说不是问题,但TextView也有一些更复杂、更高级的使用,例如显示HTML格式的富文本。
首先,为什么还要用TextView来显示html富文本呢?这里就有比较多的考量了:商品的详情页面,显示富文本、甚至显示一个比较复杂的html页面内容,都是很常见的需求。一般来说,使用WebView是最直截了当的办法,不过大家也知道,Android的WebView着实有不少的坑,无论是不同Android版本还是其本身的资源回收等问题,都让众多Android开发人苦不堪言,而且详情展示通常只是展示,并不需要过多的类似于浏览器的用户操作。而展示同样的内容,使用TextView和WebView在内存资源上的占用显然也不是一个级别。简而言之,如果能用TextView来显示富文本,要比WebView好得多。
我们知道,给TextView设置显示内容,是通过setText(CharSequence text)方法实现的。下面看一下该方法的源码:
/**
* Sets the text to be displayed. TextView <em>does not</em> accept
* HTML-like formatting, which you can do with text strings in XML resource files.
* To style your strings, attach android.text.style.* objects to a
* {@link android.text.SpannableString}, or see the
* <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
* Available Resource Types</a> documentation for an example of setting
* formatted text in the XML resource file.
* <p/>
* When required, TextView will use {@link android.text.Spannable.Factory} to create final or
* intermediate {@link Spannable Spannables}. Likewise it will use
* {@link android.text.Editable.Factory} to create final or intermediate
* {@link Editable Editables}.
*
* If the passed text is a {@link PrecomputedText} but the parameters used to create the
* PrecomputedText mismatches with this TextView, IllegalArgumentException is thrown. To ensure
* the parameters match, you can call {@link TextView#setTextMetricsParams} before calling this.
*
* @param text text to be displayed
*
* @attr ref android.R.styleable#TextView_text
* @throws IllegalArgumentException if the passed text is a {@link PrecomputedText} but the
* parameters used to create the PrecomputedText mismatches
* with this TextView.
*/
@android.view.RemotableViewMethod
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
该方法接受的参数是CharSequence,而String就是它的实现,所以我们平时都是直接setText(“字符串”)的形式来设置文本。需要注意的是,setText(CharSequence text)方法的注释很重要,值得我们好好看一下。其中,“TextView <em>does not</em> accept
* HTML-like formatting, which you can do with text strings in XML resource files.”这句注释是跟我们今天的主题相关的,人家明明白白说了,TextView不支持HTML格式的文本,好吧,难道这样就走入死胡同了?
当然不会了。在android.text包下有个Html类,提供了多个fromHtml的方法,我们先看一下只有一个参数的:
/**
* Returns displayable styled text from the provided HTML string with the legacy flags
* {@link #FROM_HTML_MODE_LEGACY}.
*
* @deprecated use {@link #fromHtml(String, int)} instead.
*/
@Deprecated
public static Spanned fromHtml(String source) {
return fromHtml(source, FROM_HTML_MODE_LEGACY, null, null);
}
虽然该方法已经deprecated了,但从它的注释中,我们可以看到,该方法输入的参数就是HTML格式的字符串,而返回的就是可显示的、带样式的文本。什么是样式?比如字体颜色大小,甚至加粗、下划线等。至于返回数据的类型Spanned,我们继续看它的源码,可知Spanned是一个带标记的文本接口,也是继承自CharSequence的。OK,既然它继承自CharSequence,那么作为setText的参数似乎是没问题的。
只说话不上代码岂是我辈风格?我们弄一个简单的富文本字符串:
public final static String HTML_TEXT =
"<p><font size=\"3\" color=\"red\">设置了字号和颜色</font></p>" +
"<b><font size=\"5\" color=\"blue\">设置字体加粗 蓝色 5号</font></font></b></br>" +
"<h1>这个是H1标签</h1></br>" +
"<p>这里显示图片:</p><img src=\"https://img0.pconline.com.cn/pconline/1808/06/11566885_13b_thumb.jpg\"";
首先,我们用最普通的方式,直接把这段字符串通过setText的方法设置给TextView(其他代码如setContentView等就不给出来了):
tvDemo.setText(HTML_TEXT);

结果就是TextView十分诚实的把你设置的字符串,很直接的显示了出来,显然这不是我们想要的结果。下面我们就用Html.fromHtml了:
tvDemo.setText(Html.fromHtml(HTML_TEXT));

很好,可以看到HTML格式的文字被正确显示了,这里包括加粗的<b>标签,段落<p>标签,1级标题<h1>标签,以及字体<font>与相应的字号、颜色等,都很好的显示了出来,与浏览器无异。不过,图片标签<img>那里,似乎并不是我们想要的结果。我们再看一下另一个fromHtml方法:
/**
* Returns displayable styled text from the provided HTML string. Any <img> tags in the
* HTML will use the specified ImageGetter to request a representation of the image (use null
* if you don't want this) and the specified TagHandler to handle unknown tags (specify null if
* you don't want this).
*
* <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
*/
public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter,
TagHandler tagHandler)
该方法同样可以处理HTML格式的富文本,而且还能依赖你指定的ImageGetter实现图片的加载。如果ImageGetter为null呢?
/**
* Returns displayable styled text from the provided HTML string. Any <img> tags in the
* HTML will display as a generic replacement image which your program can then go through and
* replace with real images.
*
* <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
*/
public static Spanned fromHtml(String source, int flags) {
return fromHtml(source, flags, null, null);
}
可以看到这个方法就是调用了fromHtml(String source, int flags, ImageGetter imageGetter, TagHandler tagHandler)方法,且ImageGetter为null的。此时<img>标签的图片不会被加载出来,而是显示为一个通用的替代图片,就是截图里那一小块绿色喽。为了能顺利显示图片,我们来看看ImageGetter是什么东西:
/**
* Retrieves images for HTML <img> tags.
*/
public static interface ImageGetter {
/**
* This method is called when the HTML parser encounters an
* <img> tag. The <code>source</code> argument is the
* string from the "src" attribute; the return value should be
* a Drawable representation of the image or <code>null</code>
* for a generic replacement image. Make sure you call
* setBounds() on your Drawable if it doesn't already have
* its bounds set.
*/
public Drawable getDrawable(String source);
}
原来是一个只有一个getDrawable方法的接口,实现该接口的目的就是为了获取HTML中<img>标签下的图片,需要注意的是,该方法的source参数已经被系统处理过了,就是从<img>标签里获取道的src属性,也就是图片的地址了,不需要我们做额外处理。下面我们写一个简单的实现:
tvDemo.setText(Html.fromHtml(HTML_TEXT, Html.FROM_HTML_MODE_COMPACT, new Html.ImageGetter() {
@Override
public Drawable getDrawable(final String source) {
try {
return Drawable.createFromStream(new URL(source).openStream(), "");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}, null));
然后不出所料的,你会得到一个android.os.NetworkOnMainThreadException的错误。为什么?很显然,Drawable.createFromStream(new URL(source).openStream(), "");这里是从网上下载图片,刚才的代码很直接的写在了主线程里。而Android不允许在主线程里访问网络(应该是4.0时候的改动吧),所以需要用子线程:
private LevelListDrawable mDrawable = new LevelListDrawable();
// 注意啦,这么写Handler是会造成内存泄漏的,实际项目中不要这么直接用。
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1123) { // 使用1123仅仅是因为在11月23号写的
Bitmap bitmap = (Bitmap)msg.obj;
BitmapDrawable drawable = new BitmapDrawable(null, bitmap);
mDrawable.addLevel(1, 1, drawable);
mDrawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
mDrawable.setLevel(1);
CharSequence charSequence = tvDemo.getText();
tvDemo.setText(charSequence);
tvDemo.invalidate();
}
}
};
// 这部分应该写在onCreate里了
tvDemo.setText(Html.fromHtml(HTML_TEXT, Html.FROM_HTML_MODE_COMPACT, new Html.ImageGetter() {
@Override
public Drawable getDrawable(final String source) {
new Thread(new Runnable() {
@Override
public void run() {
mDrawable.addLevel(0, 0, getResources().getDrawable(R.mipmap.ic_launcher));
mDrawable.setBounds(0, 0, 200, 200);
Bitmap bitmap;
try {
bitmap = BitmapFactory.decodeStream(new URL(source).openStream());
Message msg = handler.obtainMessage();
msg.what = 1123;
msg.obj = bitmap;
handler.sendMessage(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
return mDrawable;
}
}, null));
这里我们在Thread里下载图片,最终通过Handler在主线程里设置,来看看运行结果:

OK!这下子得到我们想要的结果了。

2019年8月20日 上午11:07 1F
三星5.0手机报错
2019年10月17日 上午10:07 B1
@ dbj 有木有详细一点的报错信息?
2020年4月3日 上午11:50 2F
老哥,在不在,你那个p标签之间的空白怎么去掉的
2020年9月6日 上午10:31 3F
br标签写错了,斜杠在后面
2020年9月15日 下午3:56 B1
@ nicai 虽然在前在后都能正常解析,不过按照标准的话,你说的应该是对的,在后面是标准写法。
2020年11月19日 上午12:14 4F
图片显示obj小方块是什么问题?目前发现部分android9机器会这样 。求解答
2021年6月2日 下午5:15 B1
@ Android 有没有详细的日志呢?