通过SpannableString、SpannableStringBuilder可以很方便的给TextView加上各种各样的样式,比如不同的颜色和大小,这里就不多说了,具体可以参考下面这篇文章:
SpannableString与SpannableStringBuilder使用
TextView通过使用Html.fromHtml方法可以加载html片段,但是它支持的标签并不是很多:
private void handleStartTag(String tag, Attributes attributes) {
if (tag.equalsIgnoreCase("br")) {
} 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
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())))
复制代码
测试发现高版本Android系统中(如8.0),系统已经把span给解析了,所以不会再把span的解析回调给自定义TagHandler,这种情况下只需换一个系统没有解析的标签,最好是自定义的,比如之类的。