本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Android实现TextView加载Html标签的逻辑
关于Android富文本的实现,之前的文章也有写到过几种方式。那么在国际化前提下的富文本实现,最好的方式是通过Html来指定实现富文本了。
由于英文,马来语,印地语它们的语法顺序都和中文不同,如果按Span的方法来指定索引变色来实现,几乎是不现实的。例如:
HR from Custom Company has viewed your resume.
Custom Company的人事专员查看了你的简历。
同样的话,我们需要把公司的字段变色方法,替换字体,如果用富文本的实现方法,我们要获取到当前系统语言,根据语言if else 来写不同的substring的方法去找索引替换Span。
其实我们用Html一样可以实现富文本的实现,我们Android的TextView的 Html.fromHtml 方法是可以解析部分标签的。
如果不支持的标签我们可以自定义实现,还可以自定义标签实现原本Html实现不了的效果。
一、单标签的实现
自定义字体的工具库
/**
* 系统原生的TypefaceSpan只能使用原生的默认字体
* 如果使用自定义的字体,通过这个来实现
*/
public class MyTypefaceSpan extends MetricAffectingSpan {
private final Typeface typeface;
public MyTypefaceSpan(final Typeface typeface) {
this.typeface = typeface;
}
@Override
public void updateDrawState(final TextPaint drawState) {
apply(drawState);
}
@Override
public void updateMeasureState(final TextPaint paint) {
apply(paint);
}
private void apply(final Paint paint) {
final Typeface oldTypeface = paint.getTypeface();
final int oldStyle = oldTypeface != null ? oldTypeface.getStyle() : 0;
int fakeStyle = oldStyle & ~typeface.getStyle();
if ((fakeStyle & Typeface.BOLD) != 0) {
paint.setFakeBoldText(true);
}
if ((fakeStyle & Typeface.ITALIC) != 0) {
paint.setTextSkewX(-0.25f);
}
paint.setTypeface(typeface);
}
}
复制代码
自定义标签解析器的实现
/**
* Html的TextView标签解释
* <face></face>
*/
public class TypeFaceLabel implements Html.TagHandler {
private Typeface typeface;
private int startIndex = 0;
private int stopIndex = 0;
public TypeFaceLabel(Typeface typeface) {
this.typeface = typeface;
}
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if (tag.toLowerCase().equals("face")) {
if (opening) {
startIndex = output.length();
} else {
stopIndex = output.length();
//使用的是自定义的字体来实现
output.setSpan(new MyTypefaceSpan(typeface), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
复制代码
实现的方式:
String content = "<font color=\"#000000\">HR from </font>" +
"<face><font color=\"#0689FB\">" + item.employer_name + "</font></face>" +
"<font color=\"#000000\"> has viewed your resume.</font>";
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
tv_resume_log_content.setText(Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY, null, new TypeFaceLabel(TypefaceUtil.getSFSemobold(mContext))));
} else {
tv_resume_log_content.setText(Html.fromHtml(content, null, new TypeFaceLabel(TypefaceUtil.getSFSemobold(mContext))));
}
复制代码
效果:
二、多标签的实现
这样确实是可以实现一个简单的Html的标签解析了,但是这种方式只能解析一个自定义的Tag,如果我们想支持多个自定义Tag就会出现start end 索引的冲突问题,我们需要使用一个栈的数据结构来保存不同tag的索引。
工具类如下:
/**
* 支持的标签为
* <del>xxx</del> 中划线
* <size value='16'>xxx</size> 自定义大小文本
* <face>xxx</face> 自定义字体
*/
public class CustomerLableHandler implements Html.TagHandler {
private Typeface typeface;
private int imgRes;
public CustomerLableHandler(Typeface typeface, int imgRes) {
this.typeface = typeface;
this.imgRes = imgRes;
}
/**
* html 标签的开始下标,为了支持多个标签,使用栈管理开始下标
*/
private Stack<Integer> startIndex;
/**
* html的标签的属性值
*/
private Stack<String> propertyValue;
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if (opening) {
handlerStartTAG(tag, output, xmlReader);
} else {
handlerEndTAG(tag, output);
}
}
/**
* 处理开始的标签位
*/
private void handlerStartTAG(String tag, Editable output, XMLReader xmlReader) {
if (tag.equalsIgnoreCase("del")) {
handlerStartDEL(output);
} else if (tag.equalsIgnoreCase("size")) {
handlerStartSIZE(output, xmlReader);
}else if (tag.equalsIgnoreCase("face")){
handleStartFACE(output);
}else if (tag.equalsIgnoreCase("icon")){
handleStartICON(output);
}
}
/**
* 处理结尾的标签位
*/
private void handlerEndTAG(String tag, Editable output) {
if (tag.equalsIgnoreCase("del")) {
handlerEndDEL(output);
} else if (tag.equalsIgnoreCase("size")) {
handlerEndSIZE(output);
}else if (tag.equalsIgnoreCase("face")){
handleEndFACE(output);
}else if (tag.equalsIgnoreCase("icon")){
handleEndICON(output);
}
}
// ======================= 自定义Icon begin ↓ =========================
private void handleStartICON(Editable output) {
if (startIndex == null) {
startIndex = new Stack<>();
}
startIndex.push(output.length());
}
private void handleEndICON(Editable output) {
Drawable drawable = CommUtils.getDrawable(imgRes);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
MiddleIMarginImageSpan imageSpan = new MiddleIMarginImageSpan(drawable, 4, 0, 0);
output.setSpan(imageSpan, startIndex.pop(), output.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// ======================= 自定义Icon end ↑ =========================
// ======================= 自定义字体 begin ↓ =========================
private void handleStartFACE(Editable output) {
if (startIndex == null) {
startIndex = new Stack<>();
}
startIndex.push(output.length());
}
private void handleEndFACE(Editable output) {
output.setSpan(new CustomTypefaceSpan(typeface), startIndex.pop(), output.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// ======================= 自定义字体 end ↑ =========================
// ======================= 中划线处理 begin ↓ =========================
private void handlerStartDEL(Editable output) {
if (startIndex == null) {
startIndex = new Stack<>();
}
startIndex.push(output.length());
}
//中划线的Span
private void handlerEndDEL(Editable output) {
output.setSpan(new StrikethroughSpan(), startIndex.pop(), output.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// ======================= 中划线处理 end ↑ =========================
// ======================= 文本大小设置 begin ↓ =========================
private void handlerStartSIZE(Editable output, XMLReader xmlReader) {
if (startIndex == null) {
startIndex = new Stack<>();
}
startIndex.push(output.length());
if (propertyValue == null) {
propertyValue = new Stack<>();
}
//获取自定义标签内部的属性值
propertyValue.push(getProperty(xmlReader, "value"));
}
private void handlerEndSIZE(Editable output) {
if (!propertyValue.isEmpty()) {
try {
int value = Integer.parseInt(propertyValue.pop());
output.setSpan(new AbsoluteSizeSpan(CommUtils.dip2px(value)), startIndex.pop(), output.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// ======================= 文本大小设置 end ↑ =========================
/**
* 利用反射获取html标签的属性值
*/
private String getProperty(XMLReader xmlReader, String property) {
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);
for (int i = 0; i < len; i++) {
// 这边的property换成你自己的属性名就可以了
if (property.equals(data[i * 5 + 1])) {
return data[i * 5 + 4];
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
复制代码
内部的ImageSpan是自行封装的可以居中,居下,设置margin的Span,实现如下:
public class MiddleIMarginImageSpan extends AlignMiddleImageSpan {
private int mSpanMarginLeft = 0;
private int mSpanMarginRight = 0;
private int mOffsetY = 0;
public MiddleIMarginImageSpan(Drawable d, int verticalAlignment, int marginLeft, int marginRight) {
this(d, verticalAlignment, marginLeft, marginRight, 0);
}
public MiddleIMarginImageSpan(Drawable d, int verticalAlignment, int marginLeft, int marginRight, int offsetY) {
super(d, verticalAlignment);
mSpanMarginLeft = marginLeft;
mSpanMarginRight = marginRight;
mOffsetY = offsetY;
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
if (mSpanMarginLeft != 0 || mSpanMarginRight != 0) {
super.getSize(paint, text, start, end, fm);
Drawable d = getDrawable();
return d.getIntrinsicWidth() + mSpanMarginLeft + mSpanMarginRight;
} else {
return super.getSize(paint, text, start, end, fm);
}
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, Paint paint) {
canvas.save();
canvas.translate(0, mOffsetY);
// marginRight不用专门处理,只靠getSize()中改变即可
super.draw(canvas, text, start, end, x + mSpanMarginLeft, top, y, bottom, paint);
canvas.restore();
}
}
复制代码
public class AlignMiddleImageSpan extends ImageSpan {
public static final int ALIGN_MIDDLE = 4; // 默认垂直居中
/**
* 规定这个Span占几个字的宽度
*/
private float mFontWidthMultiple = -1f;
/**
* 是否避免父类修改FontMetrics,如果为 false 则会走父类的逻辑, 会导致FontMetrics被更改
*/
private boolean mAvoidSuperChangeFontMetrics = false;
@SuppressWarnings("FieldCanBeLocal")
private int mWidth;
private Drawable mDrawable;
private int mDrawableTintColorAttr;
/**
* @param d 作为 span 的 Drawable
* @param verticalAlignment 垂直对齐方式, 如果要垂直居中, 则使用 {@link #ALIGN_MIDDLE}
*/
public AlignMiddleImageSpan(Drawable d, int verticalAlignment) {
this(d, verticalAlignment, 0);
}
/**
* @param d 作为 span 的 Drawable
* @param verticalAlignment 垂直对齐方式, 如果要垂直居中, 则使用 {@link #ALIGN_MIDDLE}
* @param fontWidthMultiple 设置这个Span占几个中文字的宽度, 当该值 > 0 时, span 的宽度为该值*一个中文字的宽度; 当该值 <= 0 时, span 的宽度由 {@link #mAvoidSuperChangeFontMetrics} 决定
*/
public AlignMiddleImageSpan(@NonNull Drawable d, int verticalAlignment, float fontWidthMultiple) {
super(d.mutate(), verticalAlignment);
mDrawable = getDrawable();
if (fontWidthMultiple >= 0) {
mFontWidthMultiple = fontWidthMultiple;
}
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
if (mAvoidSuperChangeFontMetrics) {
Drawable d = getDrawable();
Rect rect = d.getBounds();
mWidth = rect.right;
} else {
mWidth = super.getSize(paint, text, start, end, fm);
}
if (mFontWidthMultiple > 0) {
mWidth = (int) (paint.measureText("子") * mFontWidthMultiple);
}
return mWidth;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, Paint paint) {
if (mVerticalAlignment == ALIGN_MIDDLE) {
Drawable d = mDrawable;
canvas.save();
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
int fontTop = y + fontMetricsInt.top;
int fontMetricsHeight = fontMetricsInt.bottom - fontMetricsInt.top;
int iconHeight = d.getBounds().bottom - d.getBounds().top;
int iconTop = fontTop + (fontMetricsHeight - iconHeight) / 2;
canvas.translate(x, iconTop);
d.draw(canvas);
canvas.restore();
} else {
super.draw(canvas, text, start, end, x, top, y, bottom, paint);
}
}
/**
* 是否避免父类修改FontMetrics,如果为 false 则会走父类的逻辑, 会导致FontMetrics被更改
*/
public void setAvoidSuperChangeFontMetrics(boolean avoidSuperChangeFontMetrics) {
mAvoidSuperChangeFontMetrics = avoidSuperChangeFontMetrics;
}
}
复制代码
到处就完成定义啦,这里我只定义了Drawable的加载 字体的替换,字体大小设置,中划线的实现,还有其他的效果,大家可以自行实现的,注释很详细。
英文的string:
<string name="hr_view_resume"> <![CDATA[ <font color=\"#000000\">HR from </font>
<face><font color=\"#0689FB\">%1$s</font></face>
<font color=\"#000000\"> has viewed your resume.</font>
<font><icon>1</icon> from Company</font>
<font color=\"#ff6c00\"><size value=\"25\">1500/day</size></font> <del><font color=\"#808080\"><size value=\"18\">org:20000</size></font></del>
]]> </string>
复制代码
中文的string:
<string name="hr_view_resume"> <![CDATA[ <face><font color=\"#0689FB\">%1$s</font></face>
<font color=\"#000000\">的人事专员</font>
<font color=\"#000000\">查看了你的简历</font>
<font>来自公司的<icon>1</icon></font>
<font color=\"#ff6c00\"><size value=\"25\">1500/天</size></font> <del><font color=\"#808080\"><size value=\"18\">原价:20000元</size></font></del>
]]> </string>
复制代码
Activity中的调用
val content = String.format(getString(R.string.hr_view_resume), "Custom Company")
//Html的文本展示
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
mBinding.tvHtmlText.text =
Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY, null,
CustomerLableHandler(
TypefaceUtil.getSFFlower(mContext),
R.mipmap.iv_me_red_packet
)
)
} else {
mBinding.tvHtmlText.text = Html.fromHtml(content, null,
CustomerLableHandler(
TypefaceUtil.getSFFlower(mContext),
R.mipmap.iv_me_red_packet
)
)
}
复制代码
中英文实现的效果如下:
单标签的自定义和多标签的自定义讲到这来就完成了,是不是很方便呢?常用的一些Span已经给大家封装好了,有需要的也可以看一下源码,跑一下代码。
感觉大家看到这里,如果觉得不错还请点赞支持!
如有错漏与不同意见也请评论指出!
完结!