Android富文本實(shí)現(xiàn)的幾種方式匯總
Android富文本的實(shí)現(xiàn)的幾種方式
在Android開(kāi)發(fā)過(guò)程中,最常見(jiàn)的富文本場(chǎng)景一般都是變色,點(diǎn)擊跳轉(zhuǎn),或者局部變大,而我們實(shí)現(xiàn)的方式通常分為兩種。
一種是Html的方式定義在string中,通過(guò)html標(biāo)簽變色,變大,通過(guò)占位符填充數(shù)據(jù)。一般常用于有國(guó)際化的需求。
另一種是CharSequence的setSpan設(shè)置自定義Span。功能更強(qiáng)大,細(xì)讀也更細(xì),便于精準(zhǔn)操作。一般用于沒(méi)有國(guó)際化需求的地方。
為什么有國(guó)際化相關(guān)的要求,是因?yàn)橐话鉺etSpan的方式都是添加或者根據(jù)索引替換對(duì)應(yīng)的文本,如果國(guó)際化之后中英馬等語(yǔ)言的順序都變了,自然效果就不同了。當(dāng)然也可以通過(guò)判斷語(yǔ)言進(jìn)行不同的操作。這是后話(huà)了。
一,Html的方式實(shí)現(xiàn)
1.1 占位符的處理
先看看string xml中如何處理占位符 %N代表第N個(gè)參數(shù),如%3代表的是第三個(gè)參數(shù); $是結(jié)束符;
<string name="string_test_1">學(xué)號(hào):%1$d ;姓名:%2$s ;成績(jī):%3$.2f</string>
使用的時(shí)候:
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展示的效果有所不同,這里不做展開(kāi)。其實(shí)效果大差不差。
實(shí)現(xiàn)效果:

結(jié)論:
能實(shí)現(xiàn)變色,簡(jiǎn)單的變大等簡(jiǎn)單功能,由于TextView不能解析更多的Html標(biāo)簽,由此還出現(xiàn)了一些庫(kù),讓TextView支持更多標(biāo)簽,但是我們Android實(shí)現(xiàn)富文本本身就是小功能,還得依賴(lài)庫(kù)支持更多標(biāo)簽也都用不上,得不償失啊。
如果有一些自定義的需求,我們可以使用自定義標(biāo)簽+自定義標(biāo)簽的功能,例如Html中的自定義字體
1.3 自定義Html標(biāo)簽
先定義自定義字體的Span類(lèi)
/**
* 系統(tǒng)原生的TypefaceSpan只能使用原生的默認(rèn)字體
* 如果使用自定義的字體,通過(guò)這個(gè)來(lái)實(shí)現(xiàn)
*/
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);
}
}自定義標(biāo)簽:
/**
* Html的TextView標(biāo)簽解釋
* <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();
//使用的是自定義的字體來(lái)實(shí)現(xiàn)
output.setSpan(new MyTypefaceSpan(typeface), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}定義Xml并使用,注意自定義face標(biāo)簽
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))));
}效果如下:

如果想實(shí)現(xiàn)其他的變大 下劃線(xiàn) 中劃線(xiàn)等Span效果,都可以通過(guò)自定義的Html標(biāo)簽+自定義Span實(shí)現(xiàn)相應(yīng)的效果。
二,Span的幾種實(shí)現(xiàn)方式
雖然通過(guò)Html的方式可以實(shí)現(xiàn)各種效果,但是定義的時(shí)候也太過(guò)復(fù)雜,各種定義Span 定義標(biāo)簽之類(lèi)的,有沒(méi)有更簡(jiǎn)單和直接的?
有,我們直接封裝Span就行了。
2.1 java - SpanUtil
在Java中我們可以封裝工具類(lèi)一個(gè)如下:
/**
* String字符串通過(guò)區(qū)間來(lái)改變顏色,大小,字體,下劃線(xiàn)等
*/
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;
/**
* 可點(diǎn)擊-帶下劃線(xiàn)
*/
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) {
//防止重復(fù)點(diǎn)擊
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擴(kuò)展
/**
* 將一段文字中指定range的文字改變大小
* @param range 要改變大小的文字的范圍
* @param scale 縮放值,大于1,則比其他文字大;小于1,則比其他文字??;默認(rèn)是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 要改變的顏色,默認(rèn)是紅色
*/
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 要改變的顏色,默認(rèn)是紅色
*/
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的文字添加刪除線(xiàn)
* @param range 要添加刪除線(xiàn)的文字的范圍
*/
fun CharSequence.toStrikeThrougthSpan(range: IntRange): CharSequence {
return SpannableString(this).apply {
setSpan(
StrikethroughSpan(),
range.start,
range.endInclusive,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
}
}
/**
* 將一段文字中指定range的文字添加顏色和點(diǎn)擊事件
* @param range 目標(biāo)文字的范圍
*/
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 要添加刪除線(xiàn)的文字的范圍
*/
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 要添加刪除線(xiàn)的文字的范圍
*/
fun CharSequence.toCustomTypeFaceSpan(typeface: Typeface, range: IntRange): CharSequence {
return SpannableString(this).apply {
setSpan(
CustomTypefaceSpan(typeface),
range.start,
range.endInclusive,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
}
}
/**
* 將一段文字中指定range的文字添加自定義效果,可以設(shè)置對(duì)齊方式,可以設(shè)置margin
* @param range
*/
fun CharSequence.toImageSpan(
imageRes: Int,
range: IntRange,
verticalAlignment: Int = 0, //默認(rèn)底部 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
)
}
}擴(kuò)展方法的使用
mBinding.tvTextSpan1.text = "演示一下appendXX方法的用法\n"
mBinding.tvTextSpan1.appendSizeSpan("變大變大", 1.5f)
.appendColorSpan("我要變色", color = Color.parseColor("#f0aafc"))
.appendBackgroundColorSpan("我是有底色的", color = Color.parseColor("#cacee0"))
.appendStrikeThrougthSpan("添加刪除線(xiàn)哦哦哦哦")
.appendClickSpan("來(lái)點(diǎn)我一下試試啊", isUnderlineText = true, clickAction = {
toast("哎呀,您點(diǎn)到我了呢,嘿嘿")
})
.appendImageSpan(R.mipmap.ic_launcher) //默認(rèn)的大圖什么都不加 默認(rèn)在底部對(duì)齊
.appendStyleSpan("我是粗體的") //可以是默認(rèn)粗體 斜體等
.appendImageSpan(R.mipmap.ic_launcher_round, 4, width = dp2px(35f), height = dp2px(35f))//4是居中的,限制Drawable
.appendCustomTypeFaceSpan("Xiao mi Hua wei", TypefaceUtil.getSFFlower(mActivity)) //自定義字體文件
//默認(rèn)底部對(duì)齊,加左右margin
.appendImageSpan(R.mipmap.iv_me_red_packet, maginLeft = dp2px(10f), marginRight = dp2px(10f))
//添加刪除線(xiàn)
.appendStrikeThrougthSpan("添加刪除線(xiàn)哦哦哦哦添加刪除線(xiàn)哦哦哦哦")效果:

2.3 kotlin DSL方式
如果是使用Kotlin的語(yǔ)言開(kāi)發(fā),那么還有更簡(jiǎn)單的DSL封裝方式:
第一層的DSL接口
interface DslSpannableStringBuilder {
//增加一段文字
fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null)
//添加一個(gè)圖標(biāo)
fun addImage(imageRes: Int, verticalAlignment: Int = 0, maginLeft: Int = 0, marginRight: Int = 0, width: Int = 0, height: Int = 0)
}第一層的DSL接口實(shí)現(xiàn)
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)
}
}
//添加圖標(biāo)
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 {
//設(shè)置文字顏色
fun setColor(color: Int = 0)
//設(shè)置點(diǎn)擊事件
fun setClick(useUnderLine: Boolean = true, onClick: (() -> Unit)?)
//設(shè)置縮放大小
fun setScale(scale: Float = 1.0f)
//設(shè)置自定義字體
fun setTypeface(typeface: Typeface)
//是否需要中劃線(xiàn)
fun setStrikethrough(isStrikethrough: Boolean = false)
//設(shè)置背景
fun setBackground(color: Int = Color.TRANSPARENT)
}第二層Text的DSL接口實(shí)現(xiàn)
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
}
}創(chuàng)建TextVuew的擴(kuò)展入口
//為 TextView 創(chuàng)建擴(kuò)展函數(shù),其參數(shù)為接口的擴(kuò)展函數(shù)
fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> Unit) {
//具體實(shí)現(xiàn)類(lèi)
val spanStringBuilderImpl = DslSpannableStringBuilderImpl()
spanStringBuilderImpl.init()
movementMethod = LinkMovementMethod.getInstance()
//通過(guò)實(shí)現(xiàn)類(lèi)返回SpannableStringBuilder
text = spanStringBuilderImpl.build()
}使用:
mBinding.tvTextSpan4.buildSpannableString {
addText("我已詳細(xì)閱讀并同意")
addText("測(cè)試紅色的文字顏色") {
setColor(Color.RED)
}
addText("測(cè)試白色文字加上灰色背景") {
setColor(Color.WHITE)
setBackground(Color.GRAY)
}
addText("測(cè)試文本變大了") {
setColor(Color.DKGRAY)
setScale(1.5f)
}
addImage(R.mipmap.ic_launcher)
addText("測(cè)試可以點(diǎn)擊的文本") {
setClick(true) {
toast("點(diǎn)擊文本拉啦啦")
}
}
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("測(cè)試中劃線(xiàn)是否生效") {
setStrikethrough(true)
}
}效果:

總結(jié)
如果是順序固定,效果復(fù)雜,那么可以用Span的方式。
如果順序不固定(如國(guó)際化)那么可以使用Html的方式。
總的來(lái)說(shuō),兩種方式都不算太難,都是些固定的代碼。如果需求可以看源碼。
到此這篇關(guān)于Android富文本實(shí)現(xiàn)的幾種方式的文章就介紹到這了,更多相關(guān)Android富文本實(shí)現(xiàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android邊播放邊緩存視頻框架AndroidVideoCache詳解
這篇文章主要為大家介紹了Android邊播放邊緩存視頻框架AndroidVideoCache詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
ViewPager+Fragment實(shí)現(xiàn)側(cè)滑導(dǎo)航欄
這篇文章主要為大家詳細(xì)介紹了ViewPager+Fragment實(shí)現(xiàn)側(cè)滑導(dǎo)航欄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
Flutter?LinearProgressIndicator使用指南分析
這篇文章主要為大家介紹了Flutter?LinearProgressIndicator使用指南分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
ViewPager+PagerAdapter實(shí)現(xiàn)帶指示器的引導(dǎo)頁(yè)
這篇文章主要為大家詳細(xì)介紹了ViewPager+PagerAdapter實(shí)現(xiàn)帶指示器的引導(dǎo)頁(yè),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09
Android 幀動(dòng)畫(huà)的實(shí)例詳解
這篇文章主要介紹了Android 幀動(dòng)畫(huà)的實(shí)例詳解的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家掌握這部分內(nèi)容,需要的朋友可以參考下2017-10-10
Android未讀消息拖動(dòng)氣泡示例代碼詳解(附源碼)
這篇文章主要介紹了Android未讀消息拖動(dòng)氣泡示例代碼詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
Android GridView實(shí)現(xiàn)橫向列表水平滾動(dòng)
這篇文章主要為大家詳細(xì)介紹了Android GridView實(shí)現(xiàn)橫向列表水平滾動(dòng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
android開(kāi)發(fā)教程之switch控件使用示例
這篇文章主要介紹了android開(kāi)的switch控件使用示例,需要的朋友可以參考下2014-04-04
Android編程實(shí)現(xiàn)簡(jiǎn)單設(shè)置按鈕顏色的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)簡(jiǎn)單設(shè)置按鈕顏色的方法,涉及Android控件布局與屬性設(shè)置相關(guān)操作技巧,需要的朋友可以參考下2017-03-03
解決Android 沉浸式狀態(tài)欄和華為虛擬按鍵沖突問(wèn)題
對(duì)于現(xiàn)在的 App 來(lái)說(shuō),布局頁(yè)面基本都會(huì)用到沉浸式狀態(tài)欄,單純的沉浸式狀態(tài)欄很容易解決,但是在華為手機(jī)上存在一個(gè)底部虛擬按鍵的問(wèn)題,會(huì)導(dǎo)致頁(yè)面底部和頂部出現(xiàn)很大的問(wèn)題,下面通過(guò)本文給大家分享Android 沉浸式狀態(tài)欄和華為虛擬按鍵沖突問(wèn)題,一起看看吧2017-07-07

