大家都知道Android 富文本其实就是HTML标签那些东西,但Android本身对其支持有限,今天就说说如何对其进行扩展
在Android设置富文本一般如下
String txt = "Hello World";
textView.setText(HtmlCompat.fromHtml(txt,HtmlCompat.FROM_HTML_MODE_LEGACY));
这样就可以达到加粗的效果;如果要调整字体大小以及颜色呢?有人说很简单把富文本修改成
<span style='font-size:11px;color:#FF1A1A'>Hello World</span>
其实Android中的富文本中span标签中支持的属性有限,运行后你会发现上面写法其实并不生效,那有没办法让其生效呢? 答案是可以的。
我们先从源码角度来大体梳理下fromHtml的执行流程;
Html.java
//Html.java
public static Spanned fromHtml(String source, int flags) {
return fromHtml(source, flags, null, null);
}
public static Spanned fromHtml(String source, int flags, android.text.Html.ImageGetter imageGetter,
android.text.Html.TagHandler tagHandler) {
//1、创建解析器
Parser parser = new Parser();
try {
parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
} catch (org.xml.sax.SAXNotRecognizedException e) {
...
}
//2、构建一个转换器,将html格式转化为原生的Spanned
HtmlToSpannedConverter converter =
new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags);
return converter.convert();
}
从代码可以看出非常简单,其实就是将Html的格式转化为Android可以认识的Spanned对象,这样就达到了Android支持富文本的效果了,这里面核心类就是HtmlToSpannedConverter
先看convert方法
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;
}
public Spanned convert() {
//1、mReader就是上面的解析器Parser,并绑定了当前对象
mReader.setContentHandler(this);
try {
//2、解析富文本
mReader.parse(new InputSource(new StringReader(mSource)));
} catch (IOException e) {
...
}
...
//3、返回了构造器中创建的成员变量
return mSpannableStringBuilder;
}
我们来看下ContentHandler接口有那些方法
重点关注下startElement方法,从字面意思上我们可以猜测出它是负责标签元素的解析处理的,而Parser.parse方法最终会调用到HtmlToSpannedConverter.startElement方法,
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
handleStartTag(localName, attributes);
}
private void handleStartTag(String tag, Attributes attributes) {
if (tag.equalsIgnoreCase("br")) {
// We don't need to handle this. TagSoup will ensure that there's a for each
// so we can safely emit the linebreaks when we handle the close tag.
} else if (tag.equalsIgnoreCase("p")) {
startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());
startCssStyle(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("span")) {
startCssStyle(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("strong")) {
start(mSpannableStringBuilder, new Bold());
}
...
else if (mTagHandler != null) {
mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
}
}
private void startCssStyle(Editable text, Attributes attributes) {
String style = attributes.getValue("", "style");
if (style != null) {
Matcher m = getForegroundColorPattern().matcher(style);
if (m.find()) {
int c = getHtmlColor(m.group(1));
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
m = getBackgroundColorPattern().matcher(style);
if (m.find()) {
int c = getHtmlColor(m.group(1));
if (c != -1) {
start(text, new Background(c | 0xFF000000));
}
}
m = getTextDecorationPattern().matcher(style);
if (m.find()) {
String textDecoration = m.group(1);
if (textDecoration.equalsIgnoreCase("line-through")) {
start(text, new Strikethrough());
}
}
}
}
private static void start(Editable text, Object mark) {
int len = text.length();
text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
从上面代码看出,handleStartTag方法就是解析富文本中的各种类型标签,从代码看支持有
p
ul
li
div
span
strong
b
em
cite
dfn
i
big
small
font
blockquote
tt
a
u
del
s
strike
sup
sub
img
真正解析标span标签的其实就是startCssStyle方法,从代码看该方法支持的属性有限,所以扩展span标签中属性其实一大部分就是考虑如何改写startCssStyle方法,其实类中除了startXxx方法还有endXxx方法,endCssStyle方法就是将startXxx方法中解析出的数据转变为原生的可识别数据并设置到mSpannableStringBuilder中
private static void endCssStyle(Editable text) {
...
Foreground f = getLast(text, Foreground.class);
if (f != null) {
setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));
}
}
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);
}
}
}
所以扩展span标签的思路很明确了,第一步在startCssStyle方法中解析出style标签中的属性集合,第二部在endCssStyle中对上一步解析的数据进行转化;
因为startCssStyle方法都是私有我们无法复写,所以我们可以考虑把新建二个类来替代HtmlCompat、Html;先把Android原生的二个类拷贝到自己新建的二个类中,最后你会发现编译会失败,需要稍微调整下源码
Html.java
Application application = ActivityThread.currentApplication();
可以把它替换成
public static Application getCurrentApplication() {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method method = activityThreadClass.getMethod("currentApplication");
return (Application) method.invoke(null, (Object[]) null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) {
String src = attributes.getValue("", "src");
Drawable d = null;
if (img != null) {
d = img.getDrawable(src);
}
if (d == null) {
//d = Resources.getSystem().getDrawable(com.android.internal.R.drawable.unknown_image);
//替换成下面二句
int resId = Resources.getSystem().getIdentifier("unknown_image", "drawable", "android");
d = Resources.getSystem().getDrawable(resId);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
}
int len = text.length();
text.append("\uFFFC");
text.setSpan(new ImageSpan(d, src), len, text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private int getHtmlColor(String color) {
if ((mFlags & android.text.Html.FROM_HTML_OPTION_USE_CSS_COLORS)
== android.text.Html.FROM_HTML_OPTION_USE_CSS_COLORS) {
Integer i = sColorMap.get(color.toLowerCase(Locale.US));
if (i != null) {
return i;
}
}
// return Color.getHtmlColor(color);
//替换下面
try {
return convertValueToInt(color, -1);
} catch (NumberFormatException nfe) {
return -1;
}
}
public static final int convertValueToInt(CharSequence charSeq, int defaultValue)
{
if (null == charSeq)
return defaultValue;
String nm = charSeq.toString();
// XXX This code is copied from Integer.decode() so we don't
// have to instantiate an Integer!
int value;
int sign = 1;
int index = 0;
int len = nm.length();
int base = 10;
if ('-' == nm.charAt(0)) {
sign = -1;
index++;
}
if ('0' == nm.charAt(index)) {
// Quick check for a zero by itself
if (index == (len - 1))
return 0;
char c = nm.charAt(index + 1);
if ('x' == c || 'X' == c) {
index += 2;
base = 16;
} else {
index++;
base = 8;
}
}
else if ('#' == nm.charAt(index))
{
index++;
base = 16;
}
return Integer.parseInt(nm.substring(index), base) * sign;
}
Parser类为系统自带的tagsoup库,我们为确保编译成功需在build.gradle文件添加
dependencies {
compileOnly 'org.ccil.cowan.tagsoup:tagsoup:1.2.1'
}
private void startCssStyle(Editable text, Attributes attributes) {
String style = attributes.getValue("", "style");
if (style != null) {
String[] entryArray = style.split(";");
if (entryArray != null) {
for (String entry : entryArray) {
String[] kv = entry.split(":");
if (kv == null
|| kv.length < 2
|| TextUtils.isEmpty(kv[0])
|| TextUtils.isEmpty(kv[1])) {
continue;
}
String key = kv[0];
String value = kv[1];
/**
* support font-size
*/
if ("font-size".equalsIgnoreCase(key)) {
if (!TextUtils.isEmpty(value)) {
if (value.endsWith("px")) {
int size = (int) Float.parseFloat(value.substring(0, value.length() - 2));
start(text, new Size(size));
}
}
}
//support color
if ("color".equalsIgnoreCase(key)) {
if (!TextUtils.isEmpty(value)) {
int c = getHtmlColor(value);
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
}
}
}
}
//Android Origin Code
// if (style != null) {
// Matcher m = getForegroundColorPattern().matcher(style);
// if (m.find()) {
// int c = getHtmlColor(m.group(1));
// if (c != -1) {
// start(text, new Foreground(c | 0xFF000000));
// }
// }
//
// m = getBackgroundColorPattern().matcher(style);
// if (m.find()) {
// int c = getHtmlColor(m.group(1));
// if (c != -1) {
// start(text, new Background(c | 0xFF000000));
// }
// }
//
// m = getTextDecorationPattern().matcher(style);
// if (m.find()) {
// String textDecoration = m.group(1);
// if (textDecoration.equalsIgnoreCase("line-through")) {
// start(text, new Strikethrough());
// }
// }
// }
}
private static class Size {
public int mSize;
public Size(int size) {
mSize = size;
}
}
private static void endCssStyle(Editable text) {
Strikethrough s = getLast(text, Strikethrough.class);
if (s != null) {
setSpanFromMark(text, s, new StrikethroughSpan());
}
Background b = getLast(text, Background.class);
if (b != null) {
setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor));
}
Foreground f = getLast(text, Foreground.class);
if (f != null) {
setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));
}
/**
* support font-size
*/
Size size = getLast(text, Size.class);
if (size != null) {
setSpanFromMark(text, size, new AbsoluteSizeSpan(size.mSize, true));
}
}
这样就使Android支持下面富文本样式,当然我们可以参照上述操作可以继续扩展支持其他属性等。。。。
<span style='font-size:11px;color:#FF1A1A'>Hello World</span>