一、背景:富文本开发的痛点
在移动应用开发中,富文本展示是常见需求,比如社交动态的@用户、话题标签,电商详情页的价格高亮,教育类应用的公式标注等。传统的Android富文本实现依赖SpannableStringBuilder
,虽然功能强大但存在以下痛点:
- 代码冗余 :需要手动管理Span的起始/结束位置,重复调用
setSpan()
方法 - 可读性差:链式调用不友好,多层嵌套易导致代码难以维护
- 扩展困难:新增自定义样式需要重复编写Span管理逻辑
基于此,我开发了Spanny
库------一个基于Kotlin DSL的Android富文本构建库,通过简洁的链式调用语法,将复杂的Span操作封装为可扩展的DSL接口,让富文本开发像写配置一样简单。
二、核心功能亮点
1. 开箱即用的DSL语法
通过Kotlin的扩展函数和Lambda作用域,Spanny
将传统的SpannableStringBuilder
操作转化为声明式DSL,以促销奶茶为例:
kotlin
findViewById<TextView>(R.id.tv_demo).buildDslSpannableString {
addText("夏日清凉特惠季\n") {
colorGradient(
"#FF6B6B".toColorInt(),
"#4ECDC4".toColorInt(),
"#556270".toColorInt()
).bold().relativeSize(1.8f)
.shadow(radius = 4f, dx = 2f, dy = 2f, color = "#30000000".toColorInt())
}
addText("全场冰品限时优惠\n\n") {
textColor("#556270".toColorInt()).italic().underline()
}
addText("• 即日起至8月31日,")
addText("第二杯半价!") {
backgroundColor("#FFF9C4".toColorInt()).textColor("#E91E63".toColorInt()).bold()
.click {
showToast("查看活动详情")
}.clickHighlightColor("#40FF9800".toColorInt())
}
addText("\n")
addText(" 新鲜水果冰沙系列")
addImage(context = this@MainActivity, imgId = R.drawable.ic_fruit)
addText(" 限定抹茶特调")
addImage(context = this@MainActivity, imgId = R.drawable.ic_matcha)
addText("\n")
addText(" 椰香芒果西米露")
addImage(context = this@MainActivity, imgId = R.drawable.ic_mango)
addText(" 巧克力脆脆冰")
addImage(context = this@MainActivity, imgId = R.drawable.ic_chocolate)
addText("\n")
addText("原价: ")
addText("¥35") { strikethrough().textColor(Color.GRAY) }
addText(" 特惠价: ")
addText("¥25") { textColor("#FF5252".toColorInt()).relativeSize(1.3f).bold() }
addText("\n\n")
addText("限时优惠剩余: ") { textColor("#556270".toColorInt()) }
addText("02:15:36") {
backgroundColor("#FFF9C4".toColorInt()).textColor("#E91E63".toColorInt()).bold()
.relativeSize(1.2f)
}
addText("\n\n")
addText(" 立即抢购 → ") {
backgroundColor("#FF6B6B".toColorInt()).textColor(Color.WHITE).bold()
.relativeSize(1.2f).click { showToast("开始下单") }
.clickHighlightColor("#40FF5722".toColorInt())
}
addText("\n\n")
addImage(this@MainActivity, R.drawable.ic_info)
addText(" 本活动最终解释权归商家所有") {
textColor("#9E9E9E".toColorInt()).relativeSize(
0.9f
)
}
}
代码结构清晰,每段文本的样式通过链式调用直观展示,无需关注Span的具体位置管理。
2. 覆盖全场景的样式支持
库内置了10+种常用样式,覆盖视觉展示和交互需求:
类型 | 支持样式 |
---|---|
基础样式 | 字体颜色、背景色、字体大小(绝对/相对)、粗体、斜体 |
装饰效果 | 下划线、删除线、文字阴影、上标/下标 |
交互能力 | 点击事件、点击高亮颜色 |
媒体插入 | 资源图片/Bitmap插入 |
3. 灵活的扩展机制
支持两种扩展方式满足不同需求:
方式一:导入library为Module,新增专用方法(推荐通用场景)
若需新增高频使用的样式(如动态颜色),可通过扩展DslSpanBuilder
接口实现:
kotlin
// 1. 接口新增方法
interface DslSpanBuilder {
fun dynamicColor(colors: List<Int>): DslSpanBuilder
}
// 2. 实现类添加Span
class DslSpanBuilderImpl : DslSpanBuilder {
override fun dynamicColor(colors: List<Int>) = apply {
spans.add(DynamicColorSpan(colors))
}
}
// 3. 业务代码调用
addText("动态变色文本") { dynamicColor(listOf(0xFFFF0000.toInt(), 0xFF00FF00.toInt())) }
方式二:直接传入自定义Span(快速验证场景)
对于临时需求或实验性样式,可直接通过customSpan
方法传入自定义Span实例:
kotlin
// 自定义闪烁Span
class BlinkSpan : CharacterStyle() {
private var isVisible = true
init {
Handler(Looper.getMainLooper()).postDelayed({
isVisible = !isVisible
// 触发重绘
}, 500)
}
override fun updateDrawState(tp: TextPaint) {
tp.color = if (isVisible) 0xFFFF0000.toInt() else 0xFF000000.toInt()
}
}
// 使用示例
addText("闪烁文本") { customSpan(BlinkSpan()) }
三、核心实现原理
1. DSL作用域的实现
通过为TextView
定义扩展函数buildDslSpannableString
,创建DslSpannableStringBuildImpl
实例并执行Lambda块:
kotlin
fun TextView.buildDslSpannableString(init: DslSpannableStringBuilder.() -> Unit) {
val builder = DslSpannableStringBuildImpl(SpannableStringBuilder())
builder.init()
movementMethod = LinkMovementMethod.getInstance()
text = builder.build()
}
2. Span的集中管理
DslSpanBuilderImpl
内部维护spans
集合,在addText
时统一将Span应用到文本区间:
kotlin
override fun addText(text: String, method: (DslSpanBuilder.() -> Unit)?) {
val start = lastIndex
builder.append(text)
lastIndex += text.length
val spanBuilder = DslSpanBuilderImpl()
method?.invoke(spanBuilder)
// 统一应用Span
spanBuilder.build().let { (spans, clickableSpan) ->
spans.forEach { builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) }
clickableSpan?.let { builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) }
}
}
四、快速开始
在项目的build.gradle.kts
中添加依赖:
scss
implementation("io.github.wkbin:spanny:1.0.0")
五、总结与展望
Spanny
通过Kotlin DSL将复杂的富文本操作简化为声明式代码,降低了开发门槛,同时保持了良好的扩展性。未来计划:
- 优化长文本性能(Span缓存机制)
- 新增更多交互类型(长按、滑动)
项目已开源,欢迎在GitHub仓库获取完整代码,也欢迎提交Issue和PR参与共建!