我们知道android中TextView是可以支持富文本显示的,但是需要进行一定的处理。一般来说,我们会使用:
1.使用Html.fromHtml(html)
2.可以使用HtmlSpanner
Html不需要依赖任何库但是由于自身自带的HTML支持的标签很少,不能满足我们的需求,那我们就会寻找其他解决方案,例如HtmlSpanner。
HtmlSpanner是支持大多数标签的解析库,但是也不是万能,而且已经多年不维护了。下面结合我的项目说说两者存在的缺点:
在我的项目中,其实富文本需求不大,字体样式(粗体斜体下划线),字体大小和字体颜色而已。
一、Html.fromHtml()在使用中,字体和样式和字体颜色都能识别,但是字体大小是没法识别的。查看源码可以知道:
else if (tag.equalsIgnoreCase("font")) {
startFont(mSpannableStringBuilder, attributes);
}
private void startFont(Editable text, Attributes attributes) {
String color = attributes.getValue("", "color");
String face = attributes.getValue("", "face");
if (!TextUtils.isEmpty(color)) {
int c = getHtmlColor(color);
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
if (!TextUtils.isEmpty(face)) {
start(text, new Font(face));
}
}
font标签他只处理了color和face,并没有处理size,自然也就不支持字体大小了。但是它支持h系列标签,只不过修改的是整体大小,不符合我们的要求。
运行结果:
可以发现,字体大小没有任何变化。如果使用HtmlSpanner,才能显示出字体大小效果。
二、使用HtmlSpanner
HtmlSpanner比自带的Html支持更多的标签,但是它就是不支持underline,下划线是没有识别的。
同样查看他的源码,
private void registerBuiltInHandlers() {
TagNodeHandler italicHandler = new StyledTextHandler(
new Style().setFontStyle(Style.FontStyle.ITALIC));
registerHandler("i", italicHandler);
registerHandler("em", italicHandler);
registerHandler("cite", italicHandler);
registerHandler("dfn", italicHandler);
TagNodeHandler boldHandler = new StyledTextHandler(
new Style().setFontWeight(Style.FontWeight.BOLD));
registerHandler("b", boldHandler);
registerHandler("strong", boldHandler);
TagNodeHandler marginHandler = new StyledTextHandler(
new Style().setMarginLeft(new StyleValue(2.0f, StyleValue.Unit.EM)));
registerHandler("blockquote", marginHandler);
registerHandler("ul", marginHandler);
registerHandler("ol", marginHandler);
TagNodeHandler monSpaceHandler = wrap(new MonoSpaceHandler());
registerHandler("tt", monSpaceHandler);
registerHandler("code", monSpaceHandler);
registerHandler("style", new StyleNodeHandler() );
//We wrap an alignment-handler to support
//align attributes
StyledTextHandler inlineAlignment = wrap(new StyledTextHandler());
TagNodeHandler brHandler = new NewLineHandler(1, inlineAlignment);
registerHandler("br", brHandler);
Style paragraphStyle = new Style()
.setDisplayStyle(Style.DisplayStyle.BLOCK)
.setMarginBottom(
new StyleValue(1.0f, StyleValue.Unit.EM));
TagNodeHandler pHandler = new BorderAttributeHandler(wrap(new StyledTextHandler(paragraphStyle)));
registerHandler("p", pHandler);
registerHandler("div", pHandler);
registerHandler("h1", wrap(new HeaderHandler(1.5f, 0.5f)));
registerHandler("h2", wrap(new HeaderHandler(1.4f, 0.6f)));
registerHandler("h3", wrap(new HeaderHandler(1.3f, 0.7f)));
registerHandler("h4", wrap(new HeaderHandler(1.2f, 0.8f)));
registerHandler("h5", wrap(new HeaderHandler(1.1f, 0.9f)));
registerHandler("h6", wrap(new HeaderHandler(1f, 1f)));
TagNodeHandler preHandler = new PreHandler();
registerHandler("pre", preHandler);
TagNodeHandler bigHandler = new StyledTextHandler(
new Style().setFontSize(
new StyleValue(1.25f, StyleValue.Unit.EM)));
registerHandler("big", bigHandler);
TagNodeHandler smallHandler = new StyledTextHandler(
new Style().setFontSize(
new StyleValue(0.8f, StyleValue.Unit.EM)));
registerHandler("small", smallHandler);
TagNodeHandler subHandler = new SubScriptHandler();
registerHandler("sub", subHandler);
TagNodeHandler superHandler = new SuperScriptHandler();
registerHandler("sup", superHandler);
TagNodeHandler centerHandler = new StyledTextHandler(new Style().setTextAlignment(Style.TextAlignment.CENTER));
registerHandler("center", centerHandler);
registerHandler("li", new ListItemHandler());
registerHandler("a", new LinkHandler());
registerHandler("img", new ImageHandler());
registerHandler("font", new FontHandler() );
}
发现他没有处理标签,粗体,斜体他倒是支持了。不知道是什么原因,这个项目是几年前的项目了,可能当时没支持??但是现在已经不维护了,所以想使用只能自己修改源码。
运行结果:
可以看到,htmlspammer字体大小显示了,但是下划线没有了
-----------------------------------------------------------------------------------------------------------------------------------------
所以我们想支持斜体,同时支持字体大小,这两个都不能满足我们,那解决方案呢?
其实有三种方法:
1.使用html的自定义taghandler,处理自定义标签
2.修改html源码
3.修改htmlspanner源码
我来简单分析一下这三种方法的区别,
1.使用自定义taghandler,需要对font标签进行重新字符替换,然后在自己处理结果,不仅存在一个小Bug,使用起来也很麻烦,具体思路请看:https://blog.csdn.net/bhadx520/article/details/47433203
2.修改html源码,因为Html是系统的,不能直接修改,我们需要获取源码才能进行修改,而且可能遇到某些未知的问题。
3.htmlspanner项目已经几年不维护了,而且依赖于其他库,使用起来不方便。我们要修改需要阅读大量源码在进行拓展。总体花费时间精力和实用性上,要么修改Html多出不少。
因为我现在的项目要求比较低,所以打算修改更简单的html源码。
首先新建一个MyyHtml文件,把系统的Html源码拷贝粘贴。
此时我们发现,有很多处报错的地方,要么缺少某些库,要么是某些资源找不到,下面简单举几个处理的例子。
1.
首先我们会发现导包有两个包没有:
import org.ccil.cowan.tagsoup.HTMLSchema;
import org.ccil.cowan.tagsoup.Parser;
导致这个方法出错
public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter,
Html.TagHandler tagHandler, Parser parser, int flags) {
mSource = source;
mSpannableStringBuilder = new SpannableStringBuilder();
mImageGetter = imageGetter;
mTagHandler = tagHandler;
mReader = parser;
mFlags = flags;
}
那我们就需要下载对应的jar包导入,这里是下载地址:
https://mvnrepository.com/artifact/org.ccil.cowan.tagsoup/tagsoup/1.2.1
导入之后解决了这个导包问题。
2.
//代码所在行数1196
private int getHtmlColor(String color) {
if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS)
== Html.FROM_HTML_OPTION_USE_CSS_COLORS) {
Integer i = sColorMap.get(color.toLowerCase(Locale.US));
if (i != null) {
return i;
}
}
return Color.getHtmlColor(color);
}
这个方法你会发现Color.getHtmlColor(color);没有这个方法,那么我们怎么办?首先我们要知道传进来的color是什么,打断点调试一下,结果如下:
其实是标准的十六进制颜色值,那么我们改写成这样就好了
//代码所在行数1196
private int getHtmlColor(String color) {
if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS)
== Html.FROM_HTML_OPTION_USE_CSS_COLORS) {
Integer i = sColorMap.get(color.toLowerCase(Locale.US));
if (i != null) {
return i;
}
}
return Color.parseColor(color);
}
3.
if (style[j] instanceof AbsoluteSizeSpan) {
AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
float sizeDip = s.getSize();
if (!s.getDip()) {
Application application = ActivityThread.currentApplication();
sizeDip /= application.getResources().getDisplayMetrics().density;
}
还有 Application application = ActivityThread.currentApplication(); 是没有这个方法的,我们阅读代码知道,他只是想获取屏幕密度,那么我们使用Context也可以达到一样的效果,直接改写:
Context application = MyApp.getInstance();
使用全局的Application获取Context即可。
4.
//代码行数1131
d = Resources.getSystem().
getDrawable(com.android.internal.R.drawable.unknown_image);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
//这里是获取一下默认图片,也是获取不到的,那么我们随意引入一张图片即可,可以修改成下面这样
d = Resources.getSystem().getDrawable(R.mipmap.ic_launcher);
前文我们说过,Html不处理size标签,那么我们就要在处理font标签处增加对字体大小的处理即可。
原来的代码如下:
private void startFont(Editable text, Attributes attributes) {
String color = attributes.getValue("", "color");
String face = attributes.getValue("", "face");
if (!TextUtils.isEmpty(color)) {
int c = getHtmlColor(color);
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
if (!TextUtils.isEmpty(face)) {
start(text, new Font(face));
}
}
按照他处理字体颜色的思路,我们看看start()里面做了什么:
首先有一个类Foreground:
private static class Foreground {
private int mForegroundColor;
public Foreground(int foregroundColor) {
mForegroundColor = foregroundColor;
}
}
这个类很简单,只有一个属性,代表的是字体颜色,那我们也新建一个表示字体大小的类:
private static class TextSize {
private int mTextSize;
public TextSize(int textSize) {
mTextSize = textSize;
}
}
然后看看start具体的逻辑:
private static void start(Editable text, Object mark) {
int len = text.length();
text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
说白了这里只是简单的记录标签开始的位置,同时设置span渲染模式。没有什么其他逻辑。那我们对startFont进行改写如下:
private void startFont(Editable text, Attributes attributes) {
String color = attributes.getValue("", "color");
String face = attributes.getValue("", "face");
String size = attributes.getValue("", "size");
if (!TextUtils.isEmpty(color)) {
int c = getHtmlColor(color);
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
if (!TextUtils.isEmpty(face)) {
start(text, new Font(face));
}
if (!TextUtils.isEmpty(size)) {
//使用px指定大小
if (size.contains("px")) {
size = size.split("px")[0];
} else {
//使用字号指定大小
//这里我的项目中只是用了1号 2号 4号 所以我只处理了这几个大小
switch (size) {
case "1":
size = "20";
break;
case "2":
size = "24";
break;
case "4":
size = "32";
break;
default:
size = "24";
}
}
start(text, new TextSize(Integer.parseInt(size)));
}
}
首先我也先获取到“size”标签,如果不为空,那么我们就调用start()方法记录标签的起始位置,同时记录字体大小。具体处理逻辑看我代码和注释。
接下来我们还需要修改处理便签结束时的方法,endFont(),在没有处理文字大小时,源码是这样的:
private static void endFont(Editable text) {
Font font = getLast(text, Font.class);
if (font != null) {
setSpanFromMark(text, font, new TypefaceSpan(font.mFace));
}
Foreground foreground = getLast(text, Foreground.class);
if (foreground != null) {
setSpanFromMark(text, foreground,
new ForegroundColorSpan(foreground.mForegroundColor));
}
}
确实和我们前文说的一样只处理了“face”和“color”,我们想要处理size首先看看 setSpanFromMark()里面做了什么:
private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {
int where = text.getSpanStart(mark);
text.removeSpan(mark);
int len = text.length();
if (where != len) {
for (Object span : spans) {
text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
可以看到其实这里就是进行具体的字体样式设置,主要方法就是 text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
我们不需要这么复杂,我们只需要修改文字大小,不用循环寻找样式,所以直接改写成:
private static void endFont(Editable text) {
Font font = getLast(text, Font.class);
if (font != null) {
setSpanFromMark(text, font, new TypefaceSpan(font.mFace));
}
Foreground foreground = getLast(text, Foreground.class);
if (foreground != null) {
setSpanFromMark(text, foreground,
new ForegroundColorSpan(foreground.mForegroundColor));
}
//处理字体大小
TextSize textSize = getLast(text,TextSize.class);
if (textSize!=null){
int where = text.getSpanStart(textSize);
text.removeSpan(text);
int len = text.length();
text.setSpan(new AbsoluteSizeSpan(textSize.mTextSize), where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
就是判断是否有size属性,如果有,那么我们就设置字体大小。到此我们已经大功告成,我们来看一下运行结果:
简直完美对不对!哈哈。这时候我们来看看,加上下划线和字体大小的效果,看看能不能同时显示:
可以看到字体大小和下划线都可以支持了。
后记:我在跟IOS端合作时发现,有些字体颜色不显示,这不是我们代码的bug,我看了一下富文本字段如下:
5646535634564224121235456456245545645454465465465
没错!字体颜色既然不是用color标识,而是用了style!,那么在字体颜色处理方面我们额外处理style,就可以了,相信大家看了上面的文字已经知道怎么处理了。现在比较忙,暂时不更新。