开发的时候,需要使用到富文本,如果用到了Html标签,系统不支持字体大小和加粗样式,那么就需要自己解析写.
String htmlStr2 =
"Html" +
"字体变大,色值变化"
+
"字体变大,色值变化1" +
"";
TextView htmlTv2 = findViewById(R.id.html_tv2);
htmlTv2.setText(HtmlHelper.getHtmlSpanned(htmlStr2));
String htmlStr3 =
"我已经完成" +
"80%" +
"的暑假作业";
HtmlTextView htmlTv3 = findViewById(R.id.html_tv3);
htmlTv3.setHtmlColorSize(htmlStr3);
font标签:
private void startFont(Editable text, Attributes attributes) {
String color = attributes.getValue("", "color");
String face = attributes.getValue("", "face");
if (!TextUtils.isEmpty(color)) {
int c = getHtmlColor(color);
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
if (!TextUtils.isEmpty(face)) {
start(text, new Font(face));
}
}
这里font标签,只支持color属性和face,不支持size属性.
span标签:
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 Pattern getForegroundColorPattern() {
if (sForegroundColorPattern == null) {
sForegroundColorPattern = Pattern.compile(
"(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b");
}
return sForegroundColorPattern;
}
private static Pattern getBackgroundColorPattern() {
if (sBackgroundColorPattern == null) {
sBackgroundColorPattern = Pattern.compile(
"(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b");
}
return sBackgroundColorPattern;
}
private static Pattern getTextDecorationPattern() {
if (sTextDecorationPattern == null) {
sTextDecorationPattern = Pattern.compile(
"(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b");
}
return sTextDecorationPattern;
}
系统的Span标签只支持:color,background,background-color,text-decoration属性,而不支持font-size,font-weight属性.
现在已经源码为什么不支持了,那么就需要自己来解析和编写.
//标签
public static final String NEW_FONT = "myfont";
public static final String HTML_FONT = "font";
public static final String NEW_SPAN = "myspan";
public static final String HTML_SPAN = "span";
用到的类
public class HtmlLabelBean {
public String tag;//当前Tag
public int startIndex;//tag开始角标
public int endIndex;//tag结束的角标
public int size;//字体大小
@ColorInt
public int color;//字体颜色
public String fontWeight;//字体样式,目前只是判断了是否加粗
public List ranges;
/**
* 是否加粗
*/
public boolean isBold() {
return "bold".equalsIgnoreCase(fontWeight);
}
}
if (source.contains("<" + HtmlCustomTagHandler.HTML_FONT)) {
isTransform = true;
//转化font标签
source = source.replaceAll("<" + HtmlCustomTagHandler.HTML_FONT, "<" + HtmlCustomTagHandler.NEW_FONT);
source = source.replaceAll("/" + HtmlCustomTagHandler.HTML_FONT + ">", "/" + HtmlCustomTagHandler.NEW_FONT + ">");
Log.d(HtmlCustomTagHandler.TAG, "font->myfont");
}
if (source.contains("<" + HtmlCustomTagHandler.HTML_SPAN)) {
isTransform = true;
//转化span标签
source = source.replaceAll("<" + HtmlCustomTagHandler.HTML_SPAN, "<" + HtmlCustomTagHandler.NEW_SPAN);
source = source.replaceAll("/" + HtmlCustomTagHandler.HTML_SPAN + ">", "/" + HtmlCustomTagHandler.NEW_SPAN + ">");
Log.d(HtmlCustomTagHandler.TAG, "span->myspan");
}
这里在转化之前,最后判断一下元html是否包含font和span标签,如果包含则替换,避免不必要的转化.
顺序遍历字符串的时候,用到了SDK中的Html.TagHandler,需要创建一个类继承它,Html转化支持传递自定义的.系统的方法如下:
public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler);
}
private List labelBeanList;//顺序添加的Bean
public void startFont(String tag, Editable output, XMLReader xmlReader) {
int startIndex = output.length();
HtmlLabelBean bean = new HtmlLabelBean();
bean.startIndex = startIndex;
bean.tag = tag;
String color = null;
String size = null;
//字体加粗的值CSS font-weight属性:,normal,bold,bolder,lighter,也可以指定的值(100-900,其中400是normal)
//说这么多,这里只支持bold,如果是bold则加粗,否则就不加粗
String fontWeight = null;
if (NEW_FONT.equals(tag)) {
color = attributes.get("color");
size = attributes.get("size");
} else if (NEW_SPAN.equals(tag)) {
String style = attributes.get("style");
if (!TextUtils.isEmpty(style)) {
String[] styles = style.split(";");
for (String str : styles) {
if (!TextUtils.isEmpty(str)) {
String[] value = str.split(":");
if (value[0].equals("color")) {
color = value[1];
} else if (value[0].equals("font-size")) {
size = value[1];
} else if (value[0].equals("font-weight")) {
fontWeight = value[1];
}
}
}
}
}
try {
if (!TextUtils.isEmpty(color)) {
int colorInt = Color.parseColor(color);
bean.color = colorInt;
} else {
bean.color = -1;
}
} catch (Exception e) {
bean.color = -1;
}
try {
if (!TextUtils.isEmpty(size)) {
//这里用[A-Za-z]+)?,是为了假如单位不是px,dp,sp的话,或者无单位的话,那么还可以取出数值,给出一个默认的单位
Pattern compile = Pattern.compile("^(\\d+)([A-Za-z]+)?$");
Matcher matcher = compile.matcher(size);
if (matcher.matches()) {
String group1 = matcher.group(1);//12--数值
String group2 = matcher.group(2);//px/sp/dp/无--单位-默认是px
if ("sp".equalsIgnoreCase(group2)) {
bean.size = sp2px(Integer.parseInt(group1));
} else if ("dp".equalsIgnoreCase(group2)) {
bean.size = dp2px(Integer.parseInt(group1));
} else if ("px".equalsIgnoreCase(group2)) {
bean.size = Integer.parseInt(group1);
} else {
bean.size = Integer.parseInt(group1);
}
} else {
bean.size = -1;
}
} else {
bean.size = -1;
}
} catch (Exception e) {
bean.size = -1;
}
//设置字体粗细
bean.fontWeight = fontWeight;
labelBeanList.add(bean);
Log.d(TAG, "opening:开" + "tag:<" + tag + " startIndex:" + startIndex + " 当前遍历的开的集合长度:" + labelBeanList.size());
}
注意:
再获取font-size和size属性,要判断后面的单位,因为AbsoluteSizeSpan传递的字体单位是px,所以需要把单位都要转化为px.
源代码:
/**
* Set the text size to size
physical pixels.
*/
public AbsoluteSizeSpan(int size) {
this(size, false);
}
这里是做转化的逻辑部分代码
if ("sp".equalsIgnoreCase(group2)) {
bean.size = sp2px(Integer.parseInt(group1));
} else if ("dp".equalsIgnoreCase(group2)) {
bean.size = dp2px(Integer.parseInt(group1));
} else if ("px".equalsIgnoreCase(group2)) {
bean.size = Integer.parseInt(group1);
} else {
bean.size = Integer.parseInt(group1);
}
/**
* sp-->px
*
* @param sp
* @return
*/
private static int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
MyApp.app().getResources().getDisplayMetrics());
}
/**
* dp-->px
*
* @param dp
* @return
*/
private static int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
MyApp.app().getResources().getDisplayMetrics());
}
/**
* 获取最后一个与当前tag匹配的Bean的位置
* 从后往前找
*
* @param tag
* @return
*/
private int getLastLabelByTag(String tag) {
for (int size = labelBeanList.size(), i = size - 1; i >= 0; i--) {
if (!TextUtils.isEmpty(tag) &&
!TextUtils.isEmpty(labelBeanList.get(i).tag) &&
labelBeanList.get(i).tag.equals(tag)) {
return i;
}
}
return -1;
}
/**
* 计算影响的范围
*
* @param bean
*/
private void optBeanRange(HtmlLabelBean bean) {
if (bean.ranges == null) {
bean.ranges = new ArrayList<>();
}
if (tempRemoveLabelList.size() == 0) {
HtmlLabelRangeBean range = new HtmlLabelRangeBean();
range.start = bean.startIndex;
range.end = bean.endIndex;
bean.ranges.add(range);
} else {
int size = tempRemoveLabelList.size();
//逆向找到 第一个结束位置<=当前结束位置
//逆向找到最后一个开始位置>=当前开始位置
int endRangePosition = -1;
int startRangePosition = -1;
for (int i = size - 1; i >= 0; i--) {
HtmlLabelBean bean1 = tempRemoveLabelList.get(i);
if (bean1.endIndex <= bean.endIndex) {
//找第一个
if (endRangePosition == -1)
endRangePosition = i;
}
if (bean1.startIndex >= bean.startIndex) {
//找最后一个,符合条件的都覆盖之前的
startRangePosition = i;
}
}
if (startRangePosition != -1 && endRangePosition != -1) {
HtmlLabelBean lastBean = null;
//有包含关系
for (int i = startRangePosition; i <= endRangePosition; i++) {
HtmlLabelBean removeBean = tempRemoveLabelList.get(i);
lastBean = removeBean;
HtmlLabelRangeBean range;
if (i == startRangePosition) {
range = new HtmlLabelRangeBean();
range.start = bean.startIndex;
range.end = removeBean.startIndex;
bean.ranges.add(range);
} else {
range = new HtmlLabelRangeBean();
HtmlLabelBean bean1 = tempRemoveLabelList.get(i - 1);
range.start = bean1.endIndex;
range.end = removeBean.startIndex;
bean.ranges.add(range);
}
}
HtmlLabelRangeBean range = new HtmlLabelRangeBean();
range.start = lastBean.endIndex;
range.end = bean.endIndex;
bean.ranges.add(range);
} else {
//表示将要并列添加,那么影响的范围就是自己的角标范围
HtmlLabelRangeBean range = new HtmlLabelRangeBean();
range.start = bean.startIndex;
range.end = bean.endIndex;
bean.ranges.add(range);
}
}
}
计算出的范围存储在Bean类中的ranges属性中,这里也考虑了嵌套的标签查找逻辑,所以传递进来的标签也可以是嵌套类型的.
for (HtmlLabelRangeBean range : bean.ranges) {
//设置字体颜色
if (bean.color != -1)
output.setSpan(new ForegroundColorSpan(bean.color), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//设置字体大小
// 这里AbsoluteSizeSpan默所以是px
if (bean.size != -1) {
output.setSpan(new AbsoluteSizeSpan(bean.size), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
//设置是否加粗
if (bean.isBold()) {
output.setSpan(new StyleSpan(Typeface.BOLD), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
在集合中labelBeanList删除此标签Bean
/**
* 操作删除的Bean,将其添加到删除的队列中
*
* @param removeBean
*/
private void optRemoveByAddBean(HtmlLabelBean removeBean) {
int isAdd = 0;
for (int size = tempRemoveLabelList.size(), i = size - 1; i >= 0; i--) {
HtmlLabelBean bean = tempRemoveLabelList.get(i);
if (removeBean.startIndex <= bean.startIndex && removeBean.endIndex >= bean.endIndex) {
if (isAdd == 0) {
tempRemoveLabelList.set(i, removeBean);
isAdd = 1;
} else {
//表示已经把isAdd = 1;当前删除的bean,添加到了删除队列中,如果再次找到了可以removeBean可以替代的bean,则删除
tempRemoveLabelList.remove(i);
}
}
}
if (isAdd == 0) {
tempRemoveLabelList.add(removeBean);
}
Log.d(TAG, "已经删除的完整开关结点的集合长度:" + tempRemoveLabelList.size());
}
这里需要注意:
如果删除的标签,范围包含了已经删除的标签,那么则替换,并删除其他的覆盖的.
假如此时已经遍历到了
已经删除的集合中包含了
此时要把 添加到删除的集合.
因为span2的范围包含了font2.
将span2添加到删除集合中,添加后的集合数据是: .
如果不这样处理的话,那么计算影响范围的会比较复杂.
public void endFont(String tag, Editable output, XMLReader xmlReader) {
int stopIndex = output.length();
Log.d(TAG, "opening:关" + "tag:" + tag + "/> endIndex:" + stopIndex);
int lastLabelByTag = getLastLabelByTag(tag);
if (lastLabelByTag != -1) {
HtmlLabelBean bean = labelBeanList.get(lastLabelByTag);
bean.endIndex = stopIndex;
optBeanRange(bean);
Log.d(TAG, "完整的TagBean解析完成:" + bean.toString());
for (HtmlLabelRangeBean range : bean.ranges) {
//设置字体颜色
if (bean.color != -1)
output.setSpan(new ForegroundColorSpan(bean.color), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//设置字体大小
// 这里AbsoluteSizeSpan默所以是px
if (bean.size != -1) {
output.setSpan(new AbsoluteSizeSpan(bean.size), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
//设置是否加粗
if (bean.isBold()) {
output.setSpan(new StyleSpan(Typeface.BOLD), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
//从顺序添加的集合中删除已经遍历完结束标签
labelBeanList.remove(lastLabelByTag);
optRemoveByAddBean(bean);
}
}
扩展的内容是要Spanned可以设置的样式
1.在HtmlLabelBean中增加对应的属性
2.在startFont方法中解析属性就可以,设置给Bean
3.在endFont方法中设置对应的样式.
在转化自定义htmlSpanned的时候,
1.判断是否为空,如果为空,支持给默认值
2.转化之前,判断是否需要转化,再转化,以防做不必要的转化
3.如果进行了转化,则需要在最外层包一层"" + source + ""
否则这样再遍历的时候计算的角标位置会错乱,因为计算角标的时候,是按照整个 字符串进行计算的,也可以不包,也可以是其他的标签,只是因为
标签不会有什么影响原来的样式.
4.如果没做转化,如果包含了html标签,那么使用系统自带支持的html就可以的就可以.
5.如果没做转化,如果不包含了标签,那么不需要使用html.
6.创建了一个HtmlTextView,这样布局中,或者代码创建,也可以直接使用,或者直接使用HtmlHelper类中的getHtmlSpanned.
如果需要也可以下载源码.源码下载在顶部.