SpannableString — 是Android SDK中android.text包下的一个类,允许对TextView中同一文本字符串的不同部分应用多种样式。与HTML标记不同,SpannableString在Span对象级别工作,管理文本的视觉显示:颜色、大小、字体、下划线和交互元素。根据Google Developers,SpannableString被用于Android系统组件中的链接格式化。这是无需引入第三方库即可实现文本样式化的主要方式。
要点
SpannableString — 是一个实现Spannable接口的Android类,它将文本与一组管理视觉显示的Span对象一起存储。与普通String不同,SpannableString允许将样式属性绑定到特定字符范围:使部分文本变为红色、增大标题中的字体或在段落内添加可点击的链接。
该类位于android.text包中,自API Level 1起可用。SpannableString是不可变的——创建后其结构被固定,替换文本需要创建新对象。对于动态编辑,使用SpannableStringBuilder,它支持在不丢失样式的情况下插入和删除字符。
CharSequence — 是文本数据的基础接口,由String、StringBuilder和SpannableString实现。SpannableString与String的主要区别在于支持将任意对象绑定到子字符串。TextView识别Spannable接口,并在渲染时将span对象应用于文本的相应部分。如果向TextView传递普通String,则不会应用任何样式。
SpannableString将文本存储为char[]数组,并单独存储一个包含起始和结束位置元数据的span对象数组。当调用setSpan(what, start, end, flags)时,what对象与范围信息一起保存在列表中。在渲染时,TextView依次应用落在显示范围内的所有span,调用updateDrawState和updateMeasureState方法。
CharacterStyle — 是影响单个字符(无论其行位置如何)的span的基类。包括ForegroundColorSpan(文本颜色)、RelativeSizeSpan(相对大小)、StyleSpan(粗体和斜体)、UnderlineSpan(下划线)等。Character span分别应用于指定范围内的每个字符。
ParagraphStyle — 是影响整个段落的span的接口。最著名的代表是AlignmentSpan,它使整个段落左对齐、居中对齐或右对齐。Paragraph span必须覆盖整个段落,否则Android会忽略其应用。此限制是因为对齐或缩进仅对整个文本块有意义。
Span flags — 是四个常量,定义在其范围边界插入或删除文本时span的行为。SPAN_EXCLUSIVE_EXCLUSIVE仅保持span在原始边界内活动,SPAN_INCLUSIVE_INCLUSIVE在向边界添加文本时扩展它。SPAN_EXCLUSIVE_INCLUSIVE和SPAN_INCLUSIVE_EXCLUSIVE为范围的开始和结束提供混合行为。
| Flag | 左侧插入 | 右侧插入 |
|---|---|---|
| SPAN_EXCLUSIVE_EXCLUSIVE | 不包括 | 不包括 |
| SPAN_INCLUSIVE_INCLUSIVE | 包括 | 包括 |
| SPAN_EXCLUSIVE_INCLUSIVE | 不包括 | 包括 |
| SPAN_INCLUSIVE_EXCLUSIVE | 包括 | 不包括 |
正确选择flag对于EditText中的Editable文本至关重要,因为用户可以插入和删除字符。对于只读TextView,通常使用SPAN_EXCLUSIVE_EXCLUSIVE——样式仅应用于原始范围,不会因程序更改而扩展。
Android SDK提供了超过25个内置span类,涵盖了大多数文本样式化任务。每个类实现CharacterStyle或ParagraphStyle接口,并通过构造函数接收参数。所有类都位于android.text.style包中,无需连接额外依赖即可使用。
ForegroundColorSpan为指定范围设置文本颜色,以int格式接收颜色。BackgroundColorSpan为文本下方的背景着色,这对于高亮搜索查询很有用。AbsoluteSizeSpan以像素为单位设置精确字体大小,RelativeSizeSpan是相对于TextView中基础文本大小的乘数。
StyleSpan接收Typeface.NORMAL、Typeface.BOLD、Typeface.ITALIC或BOLD_ITALIC常量,并改变字符的字体样式。UnderlineSpan添加下划线,StrikethroughSpan添加删除线。SuperscriptSpan和SubscriptSpan创建上标和下标。TypefaceSpan允许通过Typeface对象为文本范围设置自定义字体。
ClickableSpan — 是用于创建可点击文本片段的抽象类。点击时调用onClick()方法。要使点击生效,TextView必须设置setMovementMethod(LinkMovementMethod.getInstance())。URLSpan是ClickableSpan的子类,用于自动打开浏览器的超链接。ClickableSpan通常与ForegroundColorSpan结合使用,以蓝色视觉突出链接。
val spannable = SpannableString("Open developer.android.com")
spannable.setSpan(
URLSpan("https://developer.android.com"),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannable.setSpan(
ForegroundColorSpan(Color.BLUE),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()
没有LinkMovementMethod,URLSpan上的点击将不会得到处理。MovementMethod负责拦截触摸事件并在触摸位置查找ClickableSpan。彩色span使链接对用户可见。
使用SpannableString从文本字符串创建实例开始,然后通过setSpan()方法依次应用span。该方法接受四个参数:span对象、起始位置、结束位置和flags。设置所有span后,通过setText()将对象传递给TextView。
让我们创建一个字符串,其中第一个单词为红色并放大了大小。为此,使用ForegroundColorSpan设置颜色和RelativeSizeSpan设置比例。两个span彼此独立地应用于同一范围——setSpan的调用顺序无关紧要。
val text = "Header: remaining text"
val spannable = SpannableString(text)
val colorSpan = ForegroundColorSpan(Color.RED)
val sizeSpan = RelativeSizeSpan(1.5f)
spannable.setSpan(colorSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(sizeSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannable
从位置0到9的字符同时获得两种样式。TextView在渲染时自动应用所有span——无需额外调用。乘数为1.5f的RelativeSizeSpan将字体大小相对于基准增加50%。
Html.fromHtml()从HTML字符串创建Spanned对象,但支持的标签集有限。SpannableString提供对每个属性的完全控制,不受HTML限制。如果需要将HTML转换为span然后添加自定义样式,可以使用Html.fromHtml()作为基础,然后通过setSpan()用span补充。
val htmlText = Html.fromHtml(
"<b>Important:</b> check the data",
Html.FROM_HTML_MODE_LEGACY
)
val spannable = SpannableString(htmlText)
spannable.setSpan(
ForegroundColorSpan(Color.RED),
0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
结果——文本“重要:”将是粗体(来自HTML)和红色(来自span)。这种方法在处理服务器内容时很方便,其中部分格式由HTML定义,部分由客户端以编程方式添加。
SpannableString允许在同一或重叠范围内应用无限数量的span。组合span是相对于HTML标记的主要优势,在HTML中嵌套标签可能发生冲突。span是独立的,在渲染时依次应用。
例如,可以使一段文本同时为粗体、红色且可点击。为此,创建三个span——StyleSpan、ForegroundColorSpan和ClickableSpan——每个都应用于同一范围。应用顺序不影响结果,因为每个span负责自己的文本特征。
如果不同类型的span部分重叠,每个span在其边界内独立工作。0–10范围上的ForegroundColorSpan和5–15范围上的StyleSpan(BOLD)将在5–10部分产生粗体红色文本,在10–15部分仅产生粗体文本。不会发生冲突,因为每个span在渲染时改变自己的属性。
getSpans(int start, int end, Class type)方法返回落在指定范围内的span数组。这对于检查已应用了哪些样式或删除特定span很有用。使用nextSpanTransition()可以遍历span的变化边界——这是需要知道样式变化位置的自定义TextView工作方式的基础。
SpannableStringBuilder — 是一个用于逐步构建样式化文本的类,支持插入、替换和删除片段。与从现成字符串创建且不可变的SpannableString不同,Builder允许依次添加文本片段并动态为其分配样式。这是复杂消息的理想选择:日志、聊天、带有动态标签的新闻标题。
Builder实现了Spannable和Editable接口,使其与EditText兼容。用户可以编辑文本,样式得以保留并在插入新字符时正确偏移。不可变的SpannableString不适合可编辑字段。
append()方法返回builder本身,从而允许构建调用链。添加文本后,通过setSpan()应用span。位置相对于builder的当前长度指定。Insert()和replace()也可用于对内容进行更精确的控制。
val builder = SpannableStringBuilder()
.append("New ")
.append("comment")
val blue = ForegroundColorSpan(Color.BLUE)
val gray = ForegroundColorSpan(Color.GRAY)
builder.setSpan(blue, 0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
builder.setSpan(gray, 6, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = builder
文本“新的 ”为蓝色,而“评论”为灰色。在它们之间插入额外字符时,由于EXCLUSIVE标志,span不会影响新文本。Builder在修改时自动调整内部索引。
使用大量span会影响TextView的渲染性能。每个span在每次重绘时调用updateDrawState()或updateMeasureState()方法。建议将每个TextView上的span数量限制在50–100个,以获得中端设备上的流畅体验。改变文本大小的span(RelativeSizeSpan、AbsoluteSizeSpan)每次更改都需要重新计算布局,这比仅用于颜色或下划线的span耗费更多资源。
TextAppearanceSpan允许通过一次setSpan()操作应用来自android:textAppearance XML资源的整套样式。不用三个单独的span(颜色、大小、字体),而是使用一个引用样式的TextAppearanceSpan。这减少了对象数量并简化了维护——资源中的样式更改会自动应用于使用此span的所有文本。
为每个setSpan()创建新的span实例会给垃圾回收器带来额外负担。最佳实践是创建常量对象用于多次应用的span。例如,ForegroundColorSpan(Color.RED)可以保存在companion object中并重复使用。但是,具有状态的span(具有不同处理器的ClickableSpan)必须为每种情况单独创建。
要诊断span性能问题,请使用Android Studio中的Layout Inspector和GPU分析器。如果带有大量span的TextView在滚动时明显卡顿,请考虑通过TextAppearanceSpan将部分span替换为静态样式,或通过将属性组合到自定义的UpdateAppearance实现中来减少span数量。
常见问题
String — 是不可变的字符序列,不支持样式。SpannableString存储相同的字符,但额外包含一个包含格式化信息的Span对象数组。TextView确定传递的CharSequence的类型,并在渲染时将span应用于相应的范围。String忽略任何样式属性,并作为纯文本显示。
removeSpan(Object span)方法删除特定的span。要完全清除,调用getSpans(0, length, Object::class.java)返回所有span的数组,然后通过removeSpan逐个删除。或者,创建不带span的新SpannableString(text.toString())。SpannableStringBuilder具有clear()方法,可同时删除文本和span。
SpannableString在EditText中工作,但对于可编辑文本,推荐使用实现Editable的SpannableStringBuilder。EditText需要Editable接口来跟踪更改。如果向EditText传递SpannableString,文本将显示样式,但在编辑时Android会将其转换为Editable,这可能会重置部分span。
在Jetpack Compose中,不直接使用Android SDK的span。相反,Compose提供了AnnotatedString——SpannableString的类似替代品,具有类似的功能:SpanStyle用于单个字符样式,ParagraphStyle用于段落。通过buildAnnotatedString遍历span可以将SpannableString转换为AnnotatedString。
创建一个继承自CharacterStyle的类,并重写updateDrawState(TextPaint tp)方法。在方法内部,更改TextPaint的属性:颜色、线宽、效果。对于度量更改,使用UpdateLayout或MetricAffectingSpan。自定义span通过setSpan()应用,与内置span相同。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。