如何扩展Android富文本之Html标签
前言
大家都知道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的执行流程;
fromHtml流程
Html.java
//Html.javapublic 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方法就是解析富文本中的各种类型标签,从代码看支持有
pullidivspanstrongbemcitedfnibigsmallfontblockquotettaudelsstrikesupsubimg
真正解析标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中对上一步解析的数据进行转化;
扩展
1. 类拷贝
因为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'}
2. 改写方法
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>
更多相关文章
- Android获取WIFI 的ssid 方法适配Android9.0
- Android 出现 OutOfMemoryError 的一种解决方法
- android keytool 不是内部命令或外部命令在 (win7下不能用的解决
- Arcgis android 10.2安装方法
- Android studio 打不开官方虚拟机 100%成功解决方法
- 输入法软键盘搜索执行两次的解决方法
- Android 开发——'Android Pre Compiler'空指针问题的解决方法
- Android 自定义TextView 实现文本间距
- android 各个span类详解--用于富文本编排 下