先贴一张效果图
介绍
通过SpannableString、SpannableStringBuilder可以很方便的给TextView加上各种各样的样式,比如不同的颜色和大小,这里就不多说了,具体可以参考下面这篇文章:
SpannableString与SpannableStringBuilder使用
TextView通过使用Html.fromHtml方法可以加载html片段,但是它支持的标签并不是很多:
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 </br> for each <br>
// so we can safely emite the linebreaks when we handle the close tag.
} else if (tag.equalsIgnoreCase("p")) {
handleP(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("div")) {
handleP(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("strong")) {
start(mSpannableStringBuilder, new Bold());
} else if (tag.equalsIgnoreCase("b")) {
start(mSpannableStringBuilder, new Bold());
} else if (tag.equalsIgnoreCase("em")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("cite")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("dfn")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("i")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("big")) {
start(mSpannableStringBuilder, new Big());
} else if (tag.equalsIgnoreCase("small")) {
start(mSpannableStringBuilder, new Small());
} else if (tag.equalsIgnoreCase("font")) {
startFont(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("blockquote")) {
handleP(mSpannableStringBuilder);
start(mSpannableStringBuilder, new Blockquote());
} else if (tag.equalsIgnoreCase("tt")) {
start(mSpannableStringBuilder, new Monospace());
} else if (tag.equalsIgnoreCase("a")) {
startA(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("u")) {
start(mSpannableStringBuilder, new Underline());
} else if (tag.equalsIgnoreCase("sup")) {
start(mSpannableStringBuilder, new Super());
} else if (tag.equalsIgnoreCase("sub")) {
start(mSpannableStringBuilder, new Sub());
} else if (tag.length() == 2 &&
Character.toLowerCase(tag.charAt(0)) == 'h' &&
tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
handleP(mSpannableStringBuilder);
start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
} else if (tag.equalsIgnoreCase("img")) {
startImg(mSpannableStringBuilder, attributes, mImageGetter);
} else if (mTagHandler != null) {
mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
}
}
查看源码应该只支持这几种,不过看最后一句代码发现它是支持自定义标签处理的,就是说你可以自己重写TagHandler去实现。
思路
结合前面说的SpannableString和参考Html类源码可以实现我们这篇文章的需求,如果你只是想解析html在TextView上显示不同的颜色,那系统已经实现了,但前提是要用font标签,比如这样:
测试TextView显示不同<font color="#C00000">颜色</font>和大小
在Html类源码中发现:
private static void startFont(SpannableStringBuilder text,
Attributes attributes) {
String color = attributes.getValue("", "color");
String face = attributes.getValue("", "face");
int len = text.length();
text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
}
private static void endFont(SpannableStringBuilder text) {
int len = text.length();
Object obj = getLast(text, Font.class);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len) {
Font f = (Font) obj;
if (!TextUtils.isEmpty(f.mColor)) {
if (f.mColor.startsWith("@")) {
Resources res = Resources.getSystem();
String name = f.mColor.substring(1);
int colorRes = res.getIdentifier(name, "color", "android");
if (colorRes != 0) {
ColorStateList colors = res.getColorStateList(colorRes, null);
text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
int c = Color.getHtmlColor(f.mColor);
if (c != -1) {
text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
if (f.mFace != null) {
text.setSpan(new TypefaceSpan(f.mFace), where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
系统处理了font标签的color和face属性,但是没有处理size属性,这个让人很郁闷,没办法,我们只有通过自定义TagHandler来处理了,这里我参考了这篇文章:
Android 多样化显示TextView以及扩展Html自定义标签
至此,TextView解析html显示不同颜色和大小的功能通过自定义TagHandler已经可以实现了,但是这种方式也有一定的局限性,就是后台给你返回的html片段的样式要使用标签中的属性,就像我上面举例的font中的color属性,但是可能后台返回的数据不一定是这样,我们后台返回的就是这样的:
<p>选项<span style='color: #FFC000; font-size: 24px;'>C</span></p>
如果是这样的情况,那就需要再对style属性进行解析,获取里面的样式属性,所以这里只是给大家提供一个思路,具体怎么处理还是要看后台返回的数据。
结尾
最后附上我处理style属性的自定义TagHandler,有不对的地方,欢迎大家指正!
package wdcloud.testdemo;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Color;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.TextAppearanceSpan;
import android.util.Log;
import org.xml.sax.XMLReader;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CustomTagHandler implements Html.TagHandler {
private final String TAG = "CustomTagHandler";
private int startIndex = 0;
private int stopIndex = 0;
private ColorStateList mOriginColors;
private Context mContext;
public CustomTagHandler(Context context,ColorStateList originColors){
mContext = context;
mOriginColors = originColors;
}
@Override
public void handleTag(boolean opening, String tag, Editable output,
XMLReader xmlReader) {
processAttributes(xmlReader);
if(tag.equalsIgnoreCase("span")){
if(opening){
startSpan(tag, output, xmlReader);
}else{
endSpan(tag, output, xmlReader);
attributes.clear();
}
}
}
public void startSpan(String tag, Editable output, XMLReader xmlReader) {
startIndex = output.length();
}
public void endSpan(String tag, Editable output, XMLReader xmlReader){
stopIndex = output.length();
String color = attributes.get("color");
String size = attributes.get("size");
String style = attributes.get("style");
if (!TextUtils.isEmpty(style)){
analysisStyle(startIndex,stopIndex,output,style);
}
if (!TextUtils.isEmpty(size)) {
size = size.split("px")[0];
}
if(!TextUtils.isEmpty(color)){
if (color.startsWith("@")) {
Resources res = Resources.getSystem();
String name = color.substring(1);
int colorRes = res.getIdentifier(name, "color", "android");
if (colorRes != 0) {
output.setSpan(new ForegroundColorSpan(colorRes), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
try {
output.setSpan(new ForegroundColorSpan(Color.parseColor(color)), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} catch (Exception e) {
e.printStackTrace();
reductionFontColor(startIndex,stopIndex,output);
}
}
}
if (!TextUtils.isEmpty(size)) {
int fontSizePx = 16;
if (null != mContext){
fontSizePx = DisplayUtil.sp2px(mContext,Integer.parseInt(size));
}
output.setSpan(new AbsoluteSizeSpan(fontSizePx), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
final HashMap<String, String> attributes = new HashMap<String, String>();
private void processAttributes(final XMLReader xmlReader) {
try {
Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
elementField.setAccessible(true);
Object element = elementField.get(xmlReader);
Field attsField = element.getClass().getDeclaredField("theAtts");
attsField.setAccessible(true);
Object atts = attsField.get(element);
Field dataField = atts.getClass().getDeclaredField("data");
dataField.setAccessible(true);
String[] data = (String[])dataField.get(atts);
Field lengthField = atts.getClass().getDeclaredField("length");
lengthField.setAccessible(true);
int len = (Integer)lengthField.get(atts);
/**
* MSH: Look for supported attributes and add to hash map.
* This is as tight as things can get :)
* The data index is "just" where the keys and values are stored.
*/
for(int i = 0; i < len; i++)
attributes.put(data[i * 5 + 1], data[i * 5 + 4]);
}
catch (Exception e) {
}
}
/**
* 还原为原来的颜色
* @param startIndex
* @param stopIndex
* @param editable
*/
private void reductionFontColor(int startIndex,int stopIndex,Editable editable){
if (null != mOriginColors){
editable.setSpan(new TextAppearanceSpan(null, 0, 0, mOriginColors, null),
startIndex, stopIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}else {
editable.setSpan(new ForegroundColorSpan(0xff2b2b2b), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
/**
* 解析style属性
* @param startIndex
* @param stopIndex
* @param editable
* @param style
*/
private void analysisStyle(int startIndex,int stopIndex,Editable editable,String style){
Log.e(TAG,"style:"+style);
String[] attrArray = style.split(";");
Map<String,String> attrMap = new HashMap<>();
if (null != attrArray){
for (String attr:attrArray){
String[] keyValueArray = attr.split(":");
if (null != keyValueArray && keyValueArray.length == 2){
// 记住要去除前后空格
attrMap.put(keyValueArray[0].trim(),keyValueArray[1].trim());
}
}
}
Log.e(TAG,"attrMap:"+attrMap.toString());
String color = attrMap.get("color");
String fontSize = attrMap.get("font-size");
if (!TextUtils.isEmpty(fontSize)) {
fontSize = fontSize.split("px")[0];
}
if(!TextUtils.isEmpty(color)){
if (color.startsWith("@")) {
Resources res = Resources.getSystem();
String name = color.substring(1);
int colorRes = res.getIdentifier(name, "color", "android");
if (colorRes != 0) {
editable.setSpan(new ForegroundColorSpan(colorRes), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
try {
editable.setSpan(new ForegroundColorSpan(Color.parseColor(color)), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} catch (Exception e) {
e.printStackTrace();
reductionFontColor(startIndex,stopIndex,editable);
}
}
}
if (!TextUtils.isEmpty(fontSize)) {
int fontSizePx = 16;
if (null != mContext){
fontSizePx = DisplayUtil.sp2px(mContext,Integer.parseInt(fontSize));
}
editable.setSpan(new AbsoluteSizeSpan(fontSizePx), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
使用方式:
TextView tv_ExtendTest = (TextView) findViewById(R.id.tv_extend_test);
tv_ExtendTest.setText(Html.fromHtml(htmlContent,null,new CustomTagHandler(TextViewExtendActivity.this,tv_ExtendTest.getTextColors())));