目录
Android富文本的实现的几种方式
在Android开发过程中,最常见的富文本场景一般都是变色,点击跳转,或者局部变大,而我们实现的方式通常分为两种。
一种是Html的方式定义在string中,通过html标签变色,变大,通过占位符填充数据。一般常用于有国际化的需求。
另一种是CharSequence的setSpan设置自定义Span。功能更强大,细读也更细,便于精准操作。一般用于没有国际化需求的地方。
为什么有国际化相关的要求,是因为一般setSpan的方式都是添加或者根据索引替换对应的文本,如果国际化之后中英马等语言的顺序都变了,自然效果就不同了。当然也可以通过判断语言进行不同的操作。这是后话了。
一,Html的方式实现
1.1 占位符的处理
先看看string xml中如何处理占位符 %N代表第N个参数,如%3代表的是第三个参数; $是结束符;
<string name="string_test_1">学号:%1$d ;姓名:%2$s ;成绩:%3$.2f</string>
使用的时候:
String testStr = getResources().getString(R.string.string_test_1); String result = String.format(testStr,1001,"张三",9.235); System.out.println(result);
1.2 Html的占位符
和上面的差不多:
<string name="purchase_points"><![CDATA[ <font color="#767676">Purchase with</font> <font color="#FF5E75">%s</font><font color="#767676"> points?</font>]]></string>
使用:
String formatPoints = PointFormatUtils.formatPoints(points); String result = String.format(getResources().getString(R.string.purchase_points),formatPoints); tv_message.setText(Html.fromHtml(result));
注:Html.fromHtml还分Android N的兼容处理,需要传入Model,不同的Model展示的效果有所不同,这里不做展开。其实效果大差不差。
实现效果:
结论:
能实现变色,简单的变大等简单功能,由于TextView不能解析更多的Html标签,由此还出现了一些库,让TextView支持更多标签,但是我们Android实现富文本本身就是小功能,还得依赖库支持更多标签也都用不上,得不偿失啊。
如果有一些自定义的需求,我们可以使用自定义标签+自定义标签的功能,例如Html中的自定义字体
1.3 自定义Html标签
先定义自定义字体的Span类
/** * 系统原生的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); } } } }
定义Xml并使用,注意自定义face标签
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)))); }
效果如下:
如果想实现其他的变大 下划线 中划线等Span效果,都可以通过自定义的Html标签+自定义Span实现相应的效果。
二,Span的几种实现方式
虽然通过Html的方式可以实现各种效果,但是定义的时候也太过复杂,各种定义Span 定义标签之类的,有没有更简单和直接的?
有,我们直接封装Span就行了。
2.1 java - SpanUtil
在Java中我们可以封装工具类一个如下:
/** * String字符串通过区间来改变颜色,大小,字体,下划线等 */ public class SpanUtils { private static final SpanUtils ourInstance = new SpanUtils(); public static SpanUtils getInstance() { return ourInstance; } private SpanUtils() { } /** * 变大变小 */ public CharSequence toSizeSpan(CharSequence charSequence, int start, int end, float scale) { SpannableString spannableString = new SpannableString(charSequence); spannableString.setSpan( new RelativeSizeSpan(scale), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); return spannableString; } /** * 变色 */ public CharSequence toColorSpan(CharSequence charSequence, int start, int end, int color) { SpannableString spannableString = new SpannableString(charSequence); spannableString.setSpan( new ForegroundColorSpan(color), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); return spannableString; } /** * 变背景色 */ public CharSequence toBackgroundColorSpan(CharSequence charSequence, int start, int end, int color) { SpannableString spannableString = new SpannableString(charSequence); spannableString.setSpan( new BackgroundColorSpan(color), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); return spannableString; } private long mLastClickTime = 0; public static final int TIME_INTERVAL = 1000; /** * 可点击-带下划线 */ public CharSequence toClickSpan(CharSequence charSequence, int start, int end, int color, boolean needUnderLine, OnSpanClickListener listener) { SpannableString spannableString = new SpannableString(charSequence); ClickableSpan clickableSpan = new ClickableSpan() { @Override public void onClick(@NonNull View widget) { if (listener != null) { //防止重复点击 if (System.currentTimeMillis() - mLastClickTime >= TIME_INTERVAL) { //to do listener.onClick(charSequence.subSequence(start, end)); mLastClickTime = System.currentTimeMillis(); } } } @Override public void updateDrawState(@NonNull TextPaint ds) { ds.setColor(color); ds.setUnderlineText(needUnderLine); } }; spannableString.setSpan( clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); return spannableString; } public interface OnSpanClickListener { void onClick(CharSequence charSequence); } /** * 变成自定义的字体 */ public CharSequence toCustomTypeFaceSpan(CharSequence charSequence, int start, int end, Typeface typeface) { SpannableString spannableString = new SpannableString(charSequence); spannableString.setSpan( new MyTypefaceSpan(typeface), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); return spannableString; } }
2.2 kotlin扩展
/** * 将一段文字中指定range的文字改变大小 * @param range 要改变大小的文字的范围 * @param scale 缩放值,大于1,则比其他文字大;小于1,则比其他文字小;默认是1.5 */ fun CharSequence.toSizeSpan(range: IntRange, scale: Float = 1.5f): CharSequence { return SpannableString(this).apply { setSpan( RelativeSizeSpan(scale), range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) } } /** * 将一段文字中指定range的文字改变前景色 * @param range 要改变前景色的文字的范围 * @param color 要改变的颜色,默认是红色 */ fun CharSequence.toColorSpan(range: IntRange, color: Int = Color.RED): CharSequence { return SpannableString(this).apply { setSpan( ForegroundColorSpan(color), range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) } } /** * 将一段文字中指定range的文字改变背景色 * @param range 要改变背景色的文字的范围 * @param color 要改变的颜色,默认是红色 */ fun CharSequence.toBackgroundColorSpan(range: IntRange, color: Int = Color.RED): CharSequence { return SpannableString(this).apply { setSpan( BackgroundColorSpan(color), range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) } } /** * 将一段文字中指定range的文字添加删除线 * @param range 要添加删除线的文字的范围 */ fun CharSequence.toStrikeThrougthSpan(range: IntRange): CharSequence { return SpannableString(this).apply { setSpan( StrikethroughSpan(), range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) } } /** * 将一段文字中指定range的文字添加颜色和点击事件 * @param range 目标文字的范围 */ fun CharSequence.toClickSpan( range: IntRange, color: Int = Color.RED, isUnderlineText: Boolean = false, clickAction: (() -> Unit)? ): CharSequence { return SpannableString(this).apply { val clickableSpan = object : ClickableSpan() { override fun onClick(widget: View) { clickAction?.invoke() } override fun updateDrawState(ds: TextPaint) { ds.color = color ds.isUnderlineText = isUnderlineText } } setSpan(clickableSpan, range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) } } /** * 将一段文字中指定range的文字添加style效果 * @param range 要添加删除线的文字的范围 */ fun CharSequence.toStyleSpan(style: Int = Typeface.BOLD, range: IntRange): CharSequence { return SpannableString(this).apply { setSpan( StyleSpan(style), range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) } } /** * 将一段文字中指定range的文字添加自定义效果 * @param range 要添加删除线的文字的范围 */ fun CharSequence.toCustomTypeFaceSpan(typeface: Typeface, range: IntRange): CharSequence { return SpannableString(this).apply { setSpan( CustomTypefaceSpan(typeface), range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) } } /** * 将一段文字中指定range的文字添加自定义效果,可以设置对齐方式,可以设置margin * @param range */ fun CharSequence.toImageSpan( imageRes: Int, range: IntRange, verticalAlignment: Int = 0, //默认底部 4是垂直居中 maginLeft: Int = 0, marginRight: Int = 0, width: Int = 0, height: Int = 0 ): CharSequence { return SpannableString(this).apply { setSpan( MiddleIMarginImageSpan( CommUtils.getDrawable(imageRes) .apply { setBounds(0, 0, if (width == 0) getIntrinsicWidth() else width, if (height == 0) getIntrinsicHeight() else height) }, verticalAlignment, maginLeft, marginRight ), range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) } }
扩展方法的使用
mBinding.tvTextSpan1.text = "演示一下appendXX方法的用法\n" mBinding.tvTextSpan1.appendSizeSpan("变大变大", 1.5f) .appendColorSpan("我要变色", color = Color.parseColor("#f0aafc")) .appendBackgroundColorSpan("我是有底色的", color = Color.parseColor("#cacee0")) .appendStrikeThrougthSpan("添加删除线哦哦哦哦") .appendClickSpan("来点我一下试试啊", isUnderlineText = true, clickAction = { toast("哎呀,您点到我了呢,嘿嘿") }) .appendImageSpan(R.mipmap.ic_launcher) //默认的大图什么都不加 默认在底部对齐 .appendStyleSpan("我是粗体的") //可以是默认粗体 斜体等 .appendImageSpan(R.mipmap.ic_launcher_round, 4, width = dp2px(35f), height = dp2px(35f))//4是居中的,限制Drawable .appendCustomTypeFaceSpan("Xiao mi Hua wei", TypefaceUtil.getSFFlower(mActivity)) //自定义字体文件 //默认底部对齐,加左右margin .appendImageSpan(R.mipmap.iv_me_red_packet, maginLeft = dp2px(10f), marginRight = dp2px(10f)) //添加删除线 .appendStrikeThrougthSpan("添加删除线哦哦哦哦添加删除线哦哦哦哦")
效果:
2.3 kotlin DSL方式
如果是使用Kotlin的语言开发,那么还有更简单的DSL封装方式:
第一层的DSL接口
interface DslSpannableStringBuilder { //增加一段文字 fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null) //添加一个图标 fun addImage(imageRes: Int, verticalAlignment: Int = 0, maginLeft: Int = 0, marginRight: Int = 0, width: Int = 0, height: Int = 0) }
第一层的DSL接口实现
class DslSpannableStringBuilderImpl : DslSpannableStringBuilder { private val builder = SpannableStringBuilder() //添加文本 override fun addText(text: String, method: (DslSpanBuilder.() -> Unit)?) { val spanBuilder = DslSpanBuilderImpl() method?.let { spanBuilder.it() } var charSeq: CharSequence = text spanBuilder.apply { if (issetColor) { charSeq = charSeq.toColorSpan(0..text.length, textColor) } if (issetBackground) { charSeq = charSeq.toBackgroundColorSpan(0..text.length, textBackgroundColor) } if (issetScale) { charSeq = charSeq.toSizeSpan(0..text.length, scaleSize) } if (isonClick) { charSeq = charSeq.toClickSpan(0..text.length, textColor, isuseUnderLine, onClick) } if (issetTypeface) { charSeq = charSeq.toCustomTypeFaceSpan(typefaces, 0..text.length) } if (issetStrikethrough) { charSeq = charSeq.toStrikeThrougthSpan(0..text.length) } builder.append(charSeq) } } //添加图标 override fun addImage(imageRes: Int, verticalAlignment: Int, maginLeft: Int, marginRight: Int, width: Int, height: Int) { var charSeq: CharSequence = "1" charSeq = charSeq.toImageSpan(imageRes, 0..1, verticalAlignment, maginLeft, marginRight, width, height) builder.append(charSeq) } fun build(): SpannableStringBuilder { return builder } }
第二层Text的DSL接口
interface DslSpanBuilder { //设置文字颜色 fun setColor(color: Int = 0) //设置点击事件 fun setClick(useUnderLine: Boolean = true, onClick: (() -> Unit)?) //设置缩放大小 fun setScale(scale: Float = 1.0f) //设置自定义字体 fun setTypeface(typeface: Typeface) //是否需要中划线 fun setStrikethrough(isStrikethrough: Boolean = false) //设置背景 fun setBackground(color: Int = Color.TRANSPARENT) }
第二层Text的DSL接口实现
class DslSpanBuilderImpl : DslSpanBuilder { var issetColor = false var textColor: Int = Color.BLACK var isonClick = false var isuseUnderLine = false var onClick: (() -> Unit)? = null var issetScale = false var scaleSize = 1.0f var issetTypeface = false var typefaces: Typeface = Typeface.DEFAULT var issetStrikethrough = false var issetBackground = false var textBackgroundColor = 0 override fun setColor(color: Int) { issetColor = true textColor = color } override fun setClick(useUnderLine: Boolean, onClick: (() -> Unit)?) { isonClick = true isuseUnderLine = useUnderLine this.onClick = onClick } override fun setScale(scale: Float) { issetScale = true scaleSize = scale } override fun setTypeface(typeface: Typeface) { issetTypeface = true typefaces = typeface } override fun setStrikethrough(isStrikethrough: Boolean) { issetStrikethrough = isStrikethrough } override fun setBackground(color: Int) { issetBackground = true textBackgroundColor = color } }
创建TextVuew的扩展入口
//为 TextView 创建扩展函数,其参数为接口的扩展函数 fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> Unit) { //具体实现类 val spanStringBuilderImpl = DslSpannableStringBuilderImpl() spanStringBuilderImpl.init() movementMethod = LinkMovementMethod.getInstance() //通过实现类返回SpannableStringBuilder text = spanStringBuilderImpl.build() }
使用:
mBinding.tvTextSpan4.buildSpannableString { addText("我已详细阅读并同意") addText("测试红色的文字颜色") { setColor(Color.RED) } addText("测试白色文字加上灰色背景") { setColor(Color.WHITE) setBackground(Color.GRAY) } addText("测试文本变大了") { setColor(Color.DKGRAY) setScale(1.5f) } addImage(R.mipmap.ic_launcher) addText("测试可以点击的文本") { setClick(true) { toast("点击文本拉啦啦") } } addImage(R.mipmap.ic_launcher_round, 5, dp2px(10f), dp2px(10f), dp2px(35f), dp2px(35f)) addText("Test Custom Typeface Font is't Success?") { setTypeface(TypefaceUtil.getSFFlower(mActivity)) } addText("测试中划线是否生效") { setStrikethrough(true) } }
效果:
总结
如果是顺序固定,效果复杂,那么可以用Span的方式。
如果顺序不固定(如国际化)那么可以使用Html的方式。
总的来说,两种方式都不算太难,都是些固定的代码。如果需求可以看源码。