一 写在开头
上一篇文章中介绍了一些 Span
理论知识,包括
Span
的定义、分类;- 富文本处理类:
SpannedString、SpannableString 、SpannableStringBuilder
; setSpan(Object what, int start, int end, int flags)
中各个参数的含义及使用示例。
文章地址:超能力文字:探索Span机制的多彩世界(一),本篇继续通过示例介绍各个Span
的用法。坐稳了,开始发车!
二 Span示例
文本如下:
ini
private const val SPAN_STR: String =
"北国风光,千里冰封,万里雪飘。\n\n" +
"望长城内外,惟余莽莽;大河上下,顿失滔滔。\n\n山舞银蛇,原驰蜡象,欲与天公试比高。\n\n" +
"须晴日,看红装素裹,_ 分外妖娆。\n\n江山如此多娇,引无数英雄竞折腰。\n\n" +
"惜秦皇汉武,略输文采;唐宗宋祖,稍逊风骚。\n\n" +
"一代天骄,成吉思汗,只识弯弓射大雕。\n\n" +
"俱往矣,数风流人物,还看今朝。"
const val SPAN_STR2 = "锄禾日当午,\n\t汗滴禾下土。\n谁知盘中餐,\n\t粒粒皆辛苦。"
const val SEG_1 = "北国风光"
const val SEG_2 = "千里冰封"
const val SEG_3 = "万里雪飘"
const val SEG_4 = "望长城内外"
const val SEG_5 = "惟余莽莽"
const val SEG_6 = "大河上下"
const val SEG_7 = "顿失滔滔"
const val SEG_8 = "山舞银蛇"
const val SEG_9 = "原驰蜡象"
const val SEG_10 = "欲与天公试比高"
const val SEG_11 = "须晴日"
const val SEG_12 = "看红装素裹"
const val SEG_13 = "_ 分外妖娆"
const val SEG_14 = "江山如此多娇"
const val SEG_15 = "引无数英雄竞折腰"
const val SEG_16 = "惜秦皇汉武"
const val SEG_17 = "略输文采"
const val SEG_18 = "唐宗宋祖"
const val SEG_19 = "稍逊风骚"
const val SEG_20 = "一代天骄"
const val SEG_21 = "成吉思汗"
const val SEG_22 = "只识弯弓射大雕"
const val SEG_23 = "俱往矣"
const val SEG_24 = "数风流人物"
const val SEG_25 = "还看今朝。"
通过 TextView
原始展示如下:
ini
val spanBuilder = SpannableStringBuilder(SPAN_STR)
tvSpan.text = spanBuilder
2.1 影响外观的Span
整体示例效果图:
2.1.1、ForegroundColorSpan
字体颜色样式,用于改变字体颜色
less
spanBuilder.setSpan(
ForegroundColorSpan(Color.RED),
index1, index1 + SEG_1.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
效果图:
2.1.2、BackgroundColorSpan
背景色样式,可以用来设定文本的背景色;
less
spanBuilder.setSpan(
BackgroundColorSpan(Color.GRAY),
index2, index2 + SEG_2.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
效果图:
2.1.3、UnderlineSpan
下划线样式,给文本添加下划线;
less
spanBuilder.setSpan(
UnderlineSpan(), index3, index3 + SEG_3.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
效果图:
2.1.4、MaskFilterSpan
应用指定的 MaskFilter
到文本,可以实现一些特殊的效果,如模糊(BlurMaskFilter
)等。
MaskFilterSpan(MaskFilter filter)设置滤镜Span,参数filter是MaskFilter类型。 BlurMaskFilter是MaskFilter子类,可以应用于Paint的一个滤镜。可以将绘制的图形进行模糊处理,以达到一些特定的视觉效果。
1、radius : 模糊半径。数值越大,模糊的范围越广。
2、style是BlurMaskFilter.Blur枚举,表示模糊的类型,包括以下四种:
--- a、NORMAL:普通模糊,应用于整个图像。
--- b、SOLID:只绘制模糊的区域,其他区域为透明。
--- c、OUTER:外部模糊,只在图像外部模糊。
--- d、INNER:内部模糊,只在图像内部模糊。
less
spanBuilder.setSpan(
//1、NORMAL:在原始边界内外都进行模糊处理。
MaskFilterSpan(BlurMaskFilter(20F, BlurMaskFilter.Blur.NORMAL)),
index4, index4 + SEG_4.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spanBuilder.setSpan(
//2、SOLID:在原始边界内部填充实心颜色,在边界外进行模糊处理。
MaskFilterSpan(BlurMaskFilter(20F, BlurMaskFilter.Blur.SOLID)),
index5, index5 + SEG_5.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spanBuilder.setSpan(
//3、OUTER:在边界内部不进行绘制,在边界外进行模糊处理。
MaskFilterSpan(BlurMaskFilter(2F, BlurMaskFilter.Blur.OUTER)),
index6, index6 + SEG_6.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spanBuilder.setSpan(
//4、INNER:在边界内部进行模糊处理,在边界外不进行绘制。
MaskFilterSpan(BlurMaskFilter(20F, BlurMaskFilter.Blur.INNER)),
index7, index7 + SEG_7.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果:
2.1.5、StrikethroughSpan
删除线样式,给文本添加删除线效果;
less
spanBuilder.setSpan(
StrikethroughSpan(),
index8, index8 + SEG_8.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
效果图:
2.1.6、ClickableSpan
设置点击事件,注意点击相关的必须设置textView.movementMethod = LinkMovementMethod.getInstance()
,否则点击不生效。
kotlin
spanBuilder.setSpan(
CustomClickSpan {showToast("自定义ClickableSpan点击: $SEG_9") },
index9, index9 + SEG_9.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
//自定义ClickableSpan
class CustomClickSpan(private val click: (View) -> Unit) : ClickableSpan() {
override fun onClick(widget: View) {
click.invoke(widget)
}
/**
* 通过修改TextPaint来修改文本状态,如修改文本颜色、下划线是否展示等
* @param paint
*/
override fun updateDrawState(paint: TextPaint) {
paint.color = Color.BLUE
paint.isUnderlineText = false
}
}
通过自定义 ClickableSpan
并重写 updateDrawState()
修改 TextPaint
,去掉了默认的下划线、修改了文字颜色。执行结果:
,点击文字时就会触发 onClick
回调了。
2.1.7、URLSpan
点击文字时进行链接跳转
less
spanBuilder.setSpan(
URLSpan("https://www.baidu.com"),
index10, index10 + SEG_10.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
效果图:
2.2 影响度量的Span
整体示例效果:
2.2.1、StyleSpan
字体样式,如通过传入的 Typeface
设置正常、粗体、斜体、加粗并倾斜等样式;
less
//设置StyleSpan:加粗、斜体等,如Typeface.NORMAL、BOLD、ITALIC、BOLD_ITALIC
spanBuilder.setSpan(
StyleSpan(Typeface.BOLD_ITALIC),
index11, index11 + SEG_11.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果:
2.2.2、TypefaceSpan
设置不同样式的文本字体
scss
val customTypeface = Typeface.create(
ResourcesCompat.getFont(this, R.font.iconfont_guide),
Typeface.BOLD_ITALIC
)
/**
* TypefaceSpan构造函数有两个:
* 1、TypefaceSpan(String family)字体族,最终通过Typeface.create(family, style)创建Typeface
* 2、TypefaceSpan(Typeface typeface)直接传入Typeface
* 当通过TypefaceSpan(String family)创建时,以前的Typeface都会保留并与新创建的合并;而通过TypefaceSpan(Typeface typeface)创建的会直接覆盖之前的Typeface设置。
*/
spanBuilder.setSpan(
//TypefaceSpan(customTypeface), //API>=28以上才有
TypefaceSpan("serif"), //系统自带的有normal,sans,monospace,serif
index12, index12 + SEG_12.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果:
2.2.3、ImageSpan
图片样式,用于在文本中插入图片;
kotlin
val imgDrawable =
ResourcesCompat.getDrawable(resources, R.drawable.icon_flower, null)
imgDrawable?.let {
it.setBounds(0, 0, 40.dp2px(), 40.dp2px())
spanBuilder.setSpan(
CenterImageSpan(it),
index13, index13 + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
//自定义ImageSpan
class CenterImageSpan(drawable: Drawable, verticalAlignment: Int = ALIGN_CENTER) :
ImageSpan(drawable, verticalAlignment) {
private var mDrawableRef: WeakReference<Drawable>? = null
/**
* @param paint Paint instance.
* @param text Current text.
* @param start Start character index for span.
* @param end End character index for span.
* @param fm Font metrics, can be null.
* @return Width of the span.
*/
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?,
): Int {
val d = getCachedDrawable()
val rect = d?.bounds
rect?.let {
if (fm != null) {
val fmInt = paint.fontMetricsInt
val imgHeight = d.bounds.height() //图片高度
val textHeight = fm.bottom - fm.top //文字行高度
val halfDiffer = (imgHeight - textHeight) / 2
if (imgHeight > textHeight && verticalAlignment == ALIGN_CENTER) {
fm.ascent = fmInt.ascent - halfDiffer
fm.descent = fmInt.descent + halfDiffer
fm.top = fmInt.top - halfDiffer
fm.bottom = fmInt.bottom + halfDiffer
} else {
fm.ascent = -rect.bottom
fm.descent = 0
fm.top = fm.ascent
fm.bottom = 0
}
}
return rect.right
}
return 0
}
private fun getCachedDrawable(): Drawable? {
val wr: WeakReference<Drawable>? = mDrawableRef
var d: Drawable? = null
if (wr != null) {
d = wr.get()
}
if (d == null) {
d = drawable
mDrawableRef = WeakReference<Drawable>(d)
}
return d
}
}
执行结果:
注意,这里使用的是自定义ImageSpan
,目的是不管图片的大小,图片都能够正常的和文字进行居中展示;而如果使用默认ImageSpan
时,如果图片的高度大于文字本身,那么居中展示就会失效。
2.2.4、RelativeSizeSpan
相对大小样式,相对于原始大小调整文本的大小;
kotlin
//自定义RelativeSizeSpan
spanBuilder.setSpan(
RelativeSizeColorSpan(0.8f, Color.RED),
index14, index14 + SEG_14.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
/**
* 自定义Span
* @param size : 文本被缩放的比例
*/
class RelativeSizeColorSpan(size: Float, @ColorInt val color: Int = Color.GRAY) :
RelativeSizeSpan(size) {
/**
* 在绘制文本时更新样式的绘制状态,例如文本颜色、下划线等。
*/
override fun updateDrawState(paint: TextPaint) {
super.updateDrawState(paint)
paint.color = color
paint.isUnderlineText = true
}
}
这里不仅想改变大小,还想改变样式(文本颜色、下划线),所以直接通过简单的自定义RelativeSizeSpan
,然后在复写的 updateDrawState()
里设置即可,执行结果(箭头方向):
2.2.5、AbsoluteSizeSpan
绝对大小样式,以指定的像素大小设置文本的大小;
java
spanBuilder.setSpan(
/**
* @param size 文本的绝对值大小,单位是像素px
* @param dip 如果为true,则size变以为dp为单位;否则还是以像素px为单位。
*/
AbsoluteSizeSpan(20, true),
index15, index15 + SEG_15.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果(箭头方向):
2.2.6、ScaleXSpan
在水平方向上缩放文本;
less
spanBuilder.setSpan(
ScaleXSpan(2f),
index16, index16 + SEG_16.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果:
2.2.7、SubscriptSpan
将文本baseline的位置向下移动,用于显示下标文本;
less
//将文本baseline的位置向下移动
spanBuilder.setSpan(
SubscriptSpan(),
index17 + SEG_17.length / 2, index17 + SEG_17.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果:
2.2.8、SuperscriptSpan
将文本baseline的位置向上移动,用于显示上标文本;
less
//将文本baseline的位置向上移动
spanBuilder.setSpan(
SuperscriptSpan(),
index18 + SEG_18.length / 2, index18 + SEG_18.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果:
2.2.9、TextAppearanceSpan
设置文本字体、大小、样式和颜色等
javascript
spanBuilder.setSpan(
TextAppearanceSpan(this, R.style.textAppearanceSpan),
index19, index19 + SEG_19.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
//styles.xml
<resources>
<style name="textAppearanceSpan" parent="@android:style/TextAppearance">
<item name="android:textColor">@color/result_points</item>
<item name="android:textSize">20sp</item>
<!-- italic、bold、normal -->
<item name="android:textStyle">italic</item>
<!-- monospace、serif、sans-serif -->
<item name="fontFamily">serif</item>
</style>
<resources>
执行结果(箭头方向):
2.3 影响段落的Span
2.3.1、LeadingMarginSpan
段落缩进,即为文本添加指定的行首缩进;
less
spanBuilder.setSpan(
/**
* first: 每个段落的首行缩进
* mRest: 每个段落的其他行缩进
*/
LeadingMarginSpan.Standard(20.dp2px(), 5.dp2px()),
0, SPAN_STR.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果: 可以看到每个段落的首行都所进行20dp
的缩进,因为每个段落都只有一行,所以针对一个段落中mRest
其他行的缩进没有体现出来而已。
2.3.2、LineBackgroundSpan
LineBackgroundSpan
改变行的背景色,如果[start,end)
不够一行按一行处理
less
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
spanBuilder.setSpan(
LineBackgroundSpan.Standard(Color.YELLOW),
0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
执行结果:
2.3.3、LineHeightSpan
改变段落的行高,可用于调整行与行之间的间距。注意,LineHeightSpan
改变的是整个段落的行高,即使它只覆盖段落的一部分。
less
//LineHeightSpan改变段落的行高
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//注意,LineHeightSpan将改变整个段落的行高,即使它只覆盖段落的一部分。
spanBuilder.setSpan(
LineHeightSpan.Standard(20.dp2px()),
0, SPAN_STR2.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
执行结果:
2.3.4、DrawableMarginSpan
在文本的行首添加一个带有指定边距的 Drawable
。
ini
val imgDrawable = ResourcesCompat.getDrawable(resources, R.drawable.icon_arrow_pull, null)
spanBuilder.setSpan(
DrawableMarginSpan(imgDrawable!!, 10.sp2px()),
0, SPAN_STR.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
执行结果:
2.3.5、IconMarginSpan
IconMarginSpan
与DrawableMarginSpan
类似,在文本的行首添加一个带有指定边距的Bitmap。
scss
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_arrow_pull), 10.sp2px()
spanBuilder.setSpan(
IconMarginSpan(bitmap),
0, SPAN_STR.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
2.3.6、QuoteSpan
QuoteSpan
可以在文本开始的地方添加引用样式(一个垂直的线条)。
scss
val quoteSpan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
/**
* QuoteSpan(int color, int stripeWidth, int gapWidth)
* @param color 垂直线条颜色。缺省情况下,条带颜色为0xff0000ff
* @param stripeWidth 线条的宽度,以像素为单位。默认值是2px。
* @param gapWidth 线条和段落之间的距离,以像素为单位。默认值是2px。
*/
QuoteSpan(Color.RED, 20, 40)
} else {
QuoteSpan(Color.RED)
}
spanBuilder.setSpan(quoteSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
执行结果:
2.3.7、BulletSpan
给文本添加项目符号(圆点样式)
kotlin
private fun getBulletSpan(): BulletSpan {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
/**
* BulletSpan(int gapWidth, int color, int bulletRadius)
* @param gapWidth: 项目符号和段落之间的距离,单位是px,默认2px
* @param color:项目符号颜色,默认是文本颜色
* @param bulletRadius:符号半径,单位是px,默认是4px
*/
BulletSpan(20, Color.RED, 10)
} else {
BulletSpan(20, Color.RED)
}
}
//第一行设置圆点
spanBuilder.setSpan(getBulletSpan(), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
val index23 = SPAN_STR.indexOf(SEG_23)
//最后一行设置圆点
spanBuilder.setSpan(getBulletSpan(),
index23, index23 + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
执行结果:
2.3.8、AlignmentSpan
文本对齐方式,AlignmentSpan.Standard
是其默认实现,其构造函数AlignmentSpan.Standard(Layout.Alignment align)
中的 align
是 Layout.Alignment
枚举类型参数,包括三种情况:ALIGN_CENTER居中、 ALIGN_NORMAL正常、ALIGN_OPPOSITE相反方向。如:
less
spanBuilder.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, SPAN_STR.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
可以看到文本已经居中展示了。
2.3.9、TabStopSpan
段落中第一行的左侧偏移量(与\t
有关系),默认实现为TabStopSpan.Standard
bash
val SPAN_STR2 = "锄禾日当午,\n\t汗滴禾下土。\n谁知盘中餐,\n\t粒粒皆辛苦。"
val spanBuilder = SpannableStringBuilder(SPAN_STR2)
spanBuilder.setSpan(TabStopSpan.Standard(40.dp2px()),
0, SPAN_STR2.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
tvSpan.text = spanBuilder
执行结果: