Spanny-使用DSL优雅构建Android富文本库

一、背景:富文本开发的痛点

在移动应用开发中,富文本展示是常见需求,比如社交动态的@用户、话题标签,电商详情页的价格高亮,教育类应用的公式标注等。传统的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参与共建!

相关推荐
用户2018792831671 小时前
图书馆书架管理员的魔法:TreeMap 的奇幻之旅
android
androidwork1 小时前
Kotlin实现文件上传进度监听:RequestBody封装详解
android·开发语言·kotlin
雨白1 小时前
从拍照到相册,安全高效地处理图片
android
androidwork1 小时前
解析401 Token过期自动刷新机制:Kotlin全栈实现指南
android·kotlin
-SOLO-1 小时前
使用Trace分析Android方法用时
android
yzpyzp1 小时前
Android 的AppBarLayout 与LinearLayput的区别
android
爱装代码的小瓶子2 小时前
字符操作函数续上
android·c语言·开发语言·数据结构·算法
用户2018792831672 小时前
故事:你的“老式弹簧售货机”(Stack<E>)
android
用户2018792831672 小时前
环形快递传送带大冒险:ArrayDeque 的奇幻之旅
android
阿古达木2 小时前
沉浸式改 bug,步步深入
前端·javascript·github