传统上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 有没有详细的日志呢?