先看效果圖
開發的時候,需要使用到富文本,如果用到了Html標籤,系統不支持字體大小和加粗樣式,那麼就需要自己解析寫.
使用例子
String htmlStr2 =
"<span style='color:#EE30A7;font-size:20px'>Html" +
"<font color='#EE2C2C' size='40px'>字體變大,色值變化</font>"
+
"<font color='#CD8500' size='60px'>字體變大,色值變化1</font>" +
"</span>";
TextView htmlTv2 = findViewById(R.id.html_tv2);
htmlTv2.setText(HtmlHelper.getHtmlSpanned(htmlStr2));
String htmlStr3 =
"<font color='#4F94CD' size='40px'>我已經完成</font>" +
"<font color='#FF0000' size='80px'>80%</font>" +
"<font color='#4F94CD' size='40px'>的暑假作業</font>";
HtmlTextView htmlTv3 = findViewById(R.id.html_tv3);
htmlTv3.setHtmlColorSize(htmlStr3);
SDK源碼:
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屬性.
現在已經源碼爲什麼不支持了,那麼就需要自己來解析和編寫.
解析流程圖
流程解析
1.獲取到了html標籤字符串
//標籤
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<HtmlLabelRangeBean> ranges;
/**
* 是否加粗
*/
public boolean isBold() {
return "bold".equalsIgnoreCase(fontWeight);
}
}
2.將制定的標籤更改爲自定義的標籤
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標籤,如果包含則替換,避免不必要的轉化.
3.順序遍歷字符串
順序遍歷字符串的時候,用到了SDK中的Html.TagHandler,需要創建一個類繼承它,Html轉化支持傳遞自定義的.系統的方法如下:
public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler);
}
3.1:查看開標籤,然後獲取其屬性,將此標籤順序存儲到開標籤集合(OPEN_LIST)
private List<HtmlLabelBean> 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 <code>size</code> 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());
}
3.2:查詢到閉標籤,則從OPEN_LIST中逆向查找與該標籤匹配的開標籤.
/**
* 獲取最後一個與當前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;
}
3.3:計算該完整標籤影響的範圍(這裏用到了DELETED_LIST集合)
/**
* 計算影響的範圍
*
* @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屬性中,這裏也考慮了嵌套的標籤查找邏輯,所以傳遞進來的標籤也可以是嵌套類型的.
3.4:設置拼接影響範圍的字體和顏色.
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);
}
}
3.5:從開始標籤中刪除該閉標籤對應的開始標籤
在集合中labelBeanList刪除此標籤Bean
3.6:把刪除的完整標籤存儲到已刪除完成標籤集合中(DELETED_LIST)
/**
* 操作刪除的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());
}
這裏需要注意:
如果刪除的標籤,範圍包含了已經刪除的標籤,那麼則替換,並刪除其他的覆蓋的.
<span1>
<font1></font1>
<span2>
<font2></font2>
</span2>
</span>
假如此時已經遍歷到了</span2>
已經刪除的集合中包含了<font1><font1> <fon2></font2>
此時要把<span2></span2>添加到刪除的集合.
因爲span2的範圍包含了font2.
將span2添加到刪除集合中,添加後的集合數據是:<font1><font1> <span2></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.如果進行了轉化,則需要在最外層包一層"<span>" + source + "</span>"
否則這樣再遍歷的時候計算的角標位置會錯亂,因爲計算角標的時候,是按照整個 字符串進行計算的,也可以不包<span></span>
,也可以是其他的標籤,只是因爲<span>
標籤不會有什麼影響原來的樣式.
4.如果沒做轉化,如果包含了html標籤,那麼使用系統自帶支持的html就可以的就可以.
5.如果沒做轉化,如果不包含了標籤,那麼不需要使用html.
6.創建了一個HtmlTextView,這樣佈局中,或者代碼創建,也可以直接使用,或者直接使用HtmlHelper類中的getHtmlSpanned.
如果需要也可以下載源碼.源碼下載在頂部.