Html能够通过Html标签来为文字设置样式,让TextView显示富文本信息,其只支持部分标签不是全部,具体支持哪些标签将分析中揭晓。
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView textView = (TextView) findViewById(R.id.tv_html); String htmlString = "
<
font
color
=
'#ff0000'
>
颜色
</
font
><
br
/>
" + "
<
a
>
链接
</
a
><>
br/>" + "
<
big
>
大字体
</
big
><
br
/>
"+ "
<
small
>
小字体
</
small
><
br
/>
"+ "
<
b
>
加粗
</
b
><
br
/>
"+ "
<
i
>
斜体
</
i
><
br
/>
" + "
<
h1
>
标题一
</
h1
>
" + "
<
h2
>
标题二
</
h2
>
" + "
<
img
src
=
'ic_launcher'
/>
" + "
<
blockquote
>
引用
</
blockquote
>
" + "
<
div
>
块
</
div
>
" + "
<
u
>
下划线
</
u
><
br
/>
" + "
<
sup
>
上标
</
sup
>
正常字体
<
sub
>
下标
</
sub
><
br
/>
" + "
<
u
><
b
><
font
color
=
'@holo_blue_light'
><
sup
><
sup
>
组
</
sup
>
合
</
sup
><
big
>
样式
</
big
><
sub
>
字
<
sub
>
体
</
sub
></
sub
></
font
></
b
></
u
>
"; textView.setText(Html.fromHtml(htmlString));}
由此可以看出Html还是比较强大的一个东西呀!
使用Html.toHtml方法能够将带有样式效果的Spanned文本对象生成对应的Html格式,标签内的字符会被转译成,下面为WebView显示效果,部分效果与上面TextView显示的效果有差异,代码如下:
webView.loadData(Html.toHtml(Html.fromHtml(htmlString)),
"text/html"
,
"utf-8"
);
显示效果还是有点差距的,用的是安卓4.0.3的手机系统,所以可能显示上有点问题,不过应该不影响大家区分。重点毕竟不在这里,大家继续往下看原理吧!
/** * 为<img>标签提供图片检索功能 */
public
static
interface
ImageGetter
{
/** * 当HTML解析器解析到<img>标签时,source参数为标签中的src的属性值, * 返回值必须为Drawable;如果返回null则会使用小方块来显示,如前面所见, * 并需要调用Drawable.setBounds()方法来设置大小,否则无法显示图片。 *
@param
source: */
public
Drawable getDrawable(String source);}
/*** HTML标签解析扩展接口*/
public
static
interface
TagHandler
{
/** * 当解析器解析到本身不支持或用户自定义的标签时,该方法会被调用 *
@param
opening:标签是否打开 *
@param
tag:标签名 *
@param
output:截止到当前标签,解析到的文本内容 *
@param
xmlReader:解析器对象 */
public
void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader);}
private
Html() { }
/** * 返回样式文本,所有<img>标签都会显示为一个小方块 * 使用TagSoup库处理HTML *
@param
source:带有html标签字符串 */
public
static
Spanned fromHtml(String source) {
return
fromHtml(source,
null
,
null
);}
/*** 可传入ImageGetter来获取图片源,TagHandler添加支持其他标签*/
public
static
Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) { .....}
/*** 将带样式文本反向解析成带Html的字符串,注意这个方法并不是还原成fromHtml接收的带Html标签文本*/
public
static
String toHtml(Spanned text) { StringBuilder out =
new
StringBuilder(); withinHtml(out, text);
return
out.toString();}
/*** 返回转译标签后的字符串*/
public
static
String escapeHtml(CharSequence text) { StringBuilder out =
new
StringBuilder(); withinStyle(out, text,
0
, text.length());
return
out.toString();}
/*** 懒加载HTML解析器的Holder* a) zygote对其进行预加载* b) 直到需要的时候才加载*/
private
static
class
HtmlParser
{
private
static
final
HTMLSchema schema =
new
HTMLSchema();}。。。。
fromHtml(String source, ImageGetter imageGetter,TagHandler tagHandler):
Html类主要方法就4个,功能也简单,生成带样式的fromHtml方法最终都是调用重载3个参数的方法。
public
static
Spanned
fromHtml
(String source, ImageGetter imageGetter, TagHandler tagHandler)
{
//初始化解析器
Parser parser =
new
Parser();
try
{
//配置解析Html模式
parser.setProperty(Parser.schemaProperty, HtmlParser.schema); }
catch
(org.xml.sax.SAXNotRecognizedException e) {
throw
new
RuntimeException(e); }
catch
(org.xml.sax.SAXNotSupportedException e) {
throw
new
RuntimeException(e); }
//初始化真正的解析器
HtmlToSpannedConverter converter =
new
HtmlToSpannedConverter(source, imageGetter, tagHandler,parser);
return
converter.convert();}
源代码中并没有包含Parser对象,而是必须导入org.ccil.cowan.tagsoup.Parser,HTML解析器是使用Tagsoup库来解析HTML标签,Tagsoup是兼容SAX的解析器,我们知道对XML常见的的解析方式还有DOM、Android系统中还使用PULL解析与SAX同样是基于事件驱动模型,使用tagsoup是因为该库可以将HTML转化为XML,我们都知道HTML有时候并不像XML那样标签都需要闭合,例如也是一个有效的标签,但是在XML中则是不良格式。详情可见官方网站,但是好像没有开发文档,这里就不详细说明,只关注SAX解析过程。
HtmlToSpannedConverter原理
class
HtmlToSpannedConverter
implements
ContentHandler
{
private
static
final
float
[] HEADER_SIZES = {
1.5f
,
1.4f
,
1.3f
,
1.2f
,
1.1f
,
1f
, };
private
String mSource;
private
XMLReader mReader;
private
SpannableStringBuilder mSpannableStringBuilder;
private
Html.ImageGetter mImageGetter;
private
Html.TagHandler mTagHandler;
public
HtmlToSpannedConverter
( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser)
{ mSource = source;
//html文本
mSpannableStringBuilder =
new
SpannableStringBuilder();
//用于存放标签中的字符串
mImageGetter = imageGetter;
//图片加载器
mTagHandler = tagHandler;
//自定义标签器
mReader = parser;
//解析器
}
public
Spanned
convert
()
{
//设置内容处理器
mReader.setContentHandler(
this
);
try
{
//开始解析
mReader.parse(
new
InputSource(
new
StringReader(mSource))); }
catch
(IOException e) {
// We are reading from a string. There should not be IO problems.
throw
new
RuntimeException(e); }
catch
(SAXException e) {
// TagSoup doesn't throw parse exceptions.
throw
new
RuntimeException(e); }
//省略
..
.
...
return
mSpannableStringBuilder;}
通过上面代码可以发现,SpannableStringBuilder是用来存放解析html标签中的字符串,类似StringBuilder,但它附带有样式的字符串。重点关注convert里面的setContentHandler方法,该方法接收的是ContentHandler接口,使用过SAX解析的读者应该不陌生,该接口定义了一系列SAX解析事件的方法。
public
interface
ContentHandler
{
//设置文档定位器
public
void
setDocumentLocator
(Locator locator)
;
//文档开始解析事件
public
void
startDocument
()
throws
SAXException
;
//文档结束解析事件
public
void
endDocument
()
throws
SAXException
;
//解析到命名空间前缀事件
public
void
startPrefixMapping
(String prefix, String uri)
throws
SAXException
;
//结束命名空间事件
public
void
endPrefixMapping
(String prefix)
throws
SAXException
;
//解析到标签事件
public
void
startElement
(String uri, String localName, String qName, Attributes atts)
throws
SAXException
;
//标签结束事件
public
void
endElement
(String uri, String localName, String qName)
throws
SAXException
;
//标签中内容事件
public
void
characters
(
char
ch[],
int
start,
int
length)
throws
SAXException
;
//可忽略的空格事件
public
void
ignorableWhitespace
(
char
ch[],
int
start,
int
length)
throws
SAXException
;
//处理指令事件
public
void
processingInstruction
(String target, String data)
throws
SAXException
;
//忽略标签事件
public
void
skippedEntity
(String name)
throws
SAXException
;}
对应HtmlToSpannedConverter中的实现。
public
void
setDocumentLocator
(Locator locator)
{}
public
void
startDocument
()
throws
SAXException
{}
public
void
endDocument
()
throws
SAXException
{}
public
void
startPrefixMapping
(String prefix, String uri)
throws
SAXException
{}
public
void
endPrefixMapping
(String prefix)
throws
SAXException
{}
public
void
startElement
(String uri, String localName, String qName, Attributes attributes)
throws
SAXException
{ handleStartTag(localName, attributes);}
public
void
endElement
(String uri, String localName, String qName)
throws
SAXException
{ handleEndTag(localName);}
public
void
characters
(
char
ch[],
int
start,
int
length)
throws
SAXException
{
//忽略
...}
public
void
ignorableWhitespace
(
char
ch[],
int
start,
int
length)
throws
SAXException
{}
public
void
processingInstruction
(String target, String data)
throws
SAXException
{}
public
void
skippedEntity
(String name)
throws
SAXException
{}
我们发现该类中只实现了startElement,endElement,characters这三个方法,所以只关心标签的类型和标签里的字符。然后调用mReader.parse方法,开始对HTML进行解析。解析的事件流如下: startElement -> characters -> endElement startElemnt里面调用的是handleStartTag方法,endElement则是调用handleEndTag方法。篇幅所限,
从
handleStartTag
方法中我们可以总结出支持的HTML标签列表:
strong
small
blockquote
monospace
h1-h6
标签是如何处理的
这里分析如何处理标签,在handleStartTag方法中可以发现br标签直接被忽略了,在handleEndTag方法中才被真正处理。
private
void
handleEndTag
(String tag)
{ ...
if
(tag.equalsIgnoreCase(
"br"
)) { handleBr(mSpannableStringBuilder); } ...}
//代码很简单,直接加换行符
private
static
void
handleBr
(SpannableStringBuilder text)
{ text.append(
"n"
);}
p标签为段落,其作用是给p标签中的文字前后换行,在handleStartTag和handleEndTag遇到p标签都是调用handleP方法,characters则添加p标签之间的字符串。
private
void
handleStartTag
(String tag, Attributes attributes)
{ ...
else
if
(tag.equalsIgnoreCase(
"p"
)) { handleP(mSpannableStringBuilder); } ...}
private
void
handleEndTag
(String tag)
{ ...
else
if
(tag.equalsIgnoreCase(
"p"
)) { handleP(mSpannableStringBuilder); } ...}
private
static
void
handleP
(SpannableStringBuilder text)
{
int
len = text.length();
if
(len >=
1
&& text.charAt(len -
1
) ==
'n'
) {
if
(len >=
2
&& text.charAt(len -
2
) ==
'n'
) {
//如果前面两个字符都为换行符,则忽略
return
; }
//否则添加一个换行符
text.append(
"n"
);
return
; }
//其他情况添加两个换行符
if
(len !=
0
) { text.append(
"nn"
); }}
strong标签
该标签作用是为加粗字体,在handleStartTag和handleEndTag分别调用start和end方法。
private
void
handleStartTag
(String tag, Attributes attributes)
{ ...
else
if
(tag.equalsIgnoreCase(
"strong"
)) { start(mSpannableStringBuilder,
new
Bold()); } ...}
private
static
class
Bold
{ }
//什么都没有
private
void
handleEndTag
(String tag)
{ ...
else
if
(tag.equalsIgnoreCase(
"strong"
)) { end(mSpannableStringBuilder, Bold.class,
new
StyleSpan(Typeface.BOLD)); } ...}
private
static
void
start
(SpannableStringBuilder text, Object mark)
{
int
len = text.length();
//mark作为类型标记并没有实际功能,指明开始的位置,
//结束位置延迟到`end`方法中处理,
//Spannable.SPAN_MARK_MARK表示当文本插入偏移时,它们仍然保持在它们的原始偏移量上。从概念上讲,文本是在标记之后添加的。
text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); }
private
static
void
end
(SpannableStringBuilder text, Class kind,Object repl)
{
//当前字符长度
int
len = text.length();
//根据kind获取最后一个set进去的对象
Object obj = getLast(text, kind);
//获取标签起始位置
int
where = text.getSpanStart(obj);
//去除标记对象
text.removeSpan(obj);
if
(where != len) {
//len则为结束的位置,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE是设置样式文字区间为闭区间
//将真正的样式对象repl设置进去,Bold对应StyleSpan类型,Typeface.BOLD 加粗样式
text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); }}
private
static
Object
getLast
(Spanned text, Class kind)
{
/* * 获取最后一个类型为king,在setSpan传入的对象 * 例如kind类型为Bold.class,则会返回在start中set进去的Bold对象 */
Object[] objs = text.getSpans(
0
, text.length(), kind);
if
(objs.length ==
0
) {
return
null
; }
else
{
//如果有期间有多个,则获取最后一个
return
objs[objs.length -
1
]; }}
经过start和end方法处理后,strong标签中的文本就被加粗,具体的样式类型这里不做详解,后续可以参考Spannable源码解析这篇目前还没人认领文章,其他为字体设置不同的样式过程一致,在handleStartTag根据不同标签类型调用start时方法传入不同对象给mark,并在handleEndTag中不同标签调用end并传入不同样式。
font标签
font标签可以给字符串指定颜色和字体。
private
void
handleStartTag
(String tag, Attributes attributes)
{ ...
else
if
(tag.equalsIgnoreCase(
"font"
)) {
//attributes带有标签中的属性
//例如<font color="#FFFFFF">,属性将以key-value的形式存在,{"color":"#FFFFFF"}。
startFont(mSpannableStringBuilder, attributes); } ...}
private
static
void
startFont
(SpannableStringBuilder text,Attributes attributes)
{ String color = attributes.getValue(
""
,
"color"
);
//获取color属性
String face = attributes.getValue(
""
,
"face"
);
//获取face属性
int
len = text.length();
//Font同样是一个用来标记属性的对象,没有实际功能
text.setSpan(
new
Font(color, face), len, len, Spannable.SPAN_MARK_MARK);}
//保存颜色值和字体类型
private
static
class
Font
{
public
String mColor;
public
String mFace;
public
Font
(String color, String face)
{ mColor = color; mFace = face; }}
private
void
handleEndTag
(String tag)
{ ...
else
if
(tag.equalsIgnoreCase(
"font"
)) { endFont(mSpannableStringBuilder); } ...}
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;
//前面与strong标签解析过程相似,多了下面处理颜色和字体的逻辑
if
(!TextUtils.isEmpty(f.mColor)) {
//如果color属性中以"@"开头,则是获取colorId对应的颜色值
//注意:只能支持android.R的资源
if
(f.mColor.startsWith(
"@"
)) { Resources res = Resources.getSystem(); String name = f.mColor.substring(
1
);
int
colorRes = res.getIdentifier(name,
"color"
,
"android"
);
if
(colorRes !=
0
) {
//也可以是color selector,则会根据不同状态显示不同颜色
ColorStateList colors = res.getColorStateList(colorRes,
null
);
//1、通过TextAppearanceSpan设置颜色
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
) {
//2、通过ForegroundColorSpan直接设置字体的rgb值
text.setSpan(
new
ForegroundColorSpan(c |
0xFF000000
), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } }
if
(f.mFace !=
null
) {
//如果有face参数则通过TypefaceSpan设置字体
text.setSpan(
new
TypefaceSpan(f.mFace), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } }}
具体支持哪些字体,在TypefaceSpan的apply方法中会先去解析对应的字体,然后绘制出来,源码如下。
private
static
void
apply
(Paint paint, String family)
{ ...
//解析字体
Typeface tf = Typeface.create(family, oldStyle); ...}
Typeface源码如下:
/*** 根据字体名称获取字体对象,如果familyName为null,则返回默认字体对象* 调用getStyle可查看该字体style属性**
@param
字体名称,可能为null*
@param
style NORMAL(标准), BOLD(粗体), ITALIC(斜体), BOLD_ITALIC(粗斜)*
@return
匹配的字体*/
public
static
Typeface
create
(String familyName,
int
style)
{
if
(sSystemFontMap !=
null
) {
//字体缓存在sSystemFontMap中
return
create(sSystemFontMap.get(familyName), style); }
return
null
;}
//init方法中初始化sSystemFontMap
private
static
void
init
()
{
// 获取字体配置文件目录
//private static File getSystemFontConfigLocation() {
//return new File("/system/etc/");
//}
File systemFontConfigLocation = getSystemFontConfigLocation();
//获取字体配置文件
//static final String FONTS_CONFIG = "fonts.xml";
File configFilename =
new
File(systemFontConfigLocation, FONTS_CONFIG);
try
{
//将字体名称更Typeface对象缓存在map中
//具体解析过程忽略,有兴趣可自行翻阅源码
.... sSystemFontMap = systemFonts; }
catch
(RuntimeException e) { .... }}
img标签
//img标签只有在标签开始时处理
private
void
handleStartTag(String tag, Attributes attributes) {
else
if
(tag.equalsIgnoreCase(
"img"
)) { startImg(mSpannableStringBuilder, attributes, mImageGetter); } ...
}
//与其他标签处理过程多了Attributes标签属性,Html.ImageGetter 自定义图片获取
private
static
void
startImg
(SpannableStringBuilder text, Attributes attributes, Html.ImageGetter img)
{
//获取src属性
String src = attributes.getValue(
""
,
"src"
); Drawable d =
null
;
if
(img !=
null
) {
//调用自定义的图片获取方式,并传入src属性值
d = img.getDrawable(src); }
if
(d ==
null
) {
//如果图片为空,则返回一个小方块
d = Resources.getSystem(). getDrawable(com.android.internal.R.drawable.unknown_image); d.setBounds(
0
,
0
, d.getIntrinsicWidth(), d.getIntrinsicHeight()); }
int
len = text.length();
//添加图片占位字符
text.append(
"uFFFC"
);
//通过使用ImageSpan设置图片效果
text.setSpan(
new
ImageSpan(d, src), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);}
自定义标签
private
void
handleStartTag
(String tag, Attributes attributes)
{ ...
else
if
(mTagHandler !=
null
) {
//通过自定义标签处理器来扩展自定义标签
mTagHandler.handleTag(
true
, tag, mSpannableStringBuilder, mReader); } ...}
private
void
handleEndTag
(String tag)
{ ...
else
if
(mTagHandler !=
null
) {
//闭合标签
mTagHandler.handleTag(
false
, tag, mSpannableStringBuilder, mReader); } ...}
关于自定义标签有个小问题是,handleTag并没有传入Attributes标签属性,所以无法直接获取自定义标签的属性值,下面给出两种方案解决这个问题:
1.通过某一部分标签名作为属性值,例如<custom>标签,我们想加入id的参数,则可将标签名变为<custom-id-123>,然后在handleTag中自行解析。
2.通过反射XMLReader来获取属性值,具体例子可参考stackoverflow:How to get an attribute from an XMLReader
convert方法剩下部分
不要忽略了parse之后还有一部分代码。
//修正段落标记范围
//ParagraphStyle为段落级别样式
Object[] obj = mSpannableStringBuilder.getSpans(
0
, mSpannableStringBuilder.length(), ParagraphStyle.class);
for
(
int
i =
0
; i < obj.length; i++) {
int
start = mSpannableStringBuilder.getSpanStart(obj[i]);
int
end = mSpannableStringBuilder.getSpanEnd(obj[i]);
// 去除末尾两个换行符
if
(end -
2
>=
0
) {
if
(mSpannableStringBuilder.charAt(end -
1
) ==
'n'
&& mSpannableStringBuilder.charAt(end -
2
) ==
'n'
) { end--; } }
if
(end == start) {
//除去没有显示的样式
mSpannableStringBuilder.removeSpan(obj[i]); }
else
{
//Spannable.SPAN_PARAGRAPH以换行符为起始点和终点
mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); }}
return
mSpannableStringBuilder;
大家都在看
返回搜狐,查看更多
责任编辑:
声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。