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参与共建!

相关推荐
何盖(何松影)2 小时前
Android T startingwindow使用总结
android
小李飞飞砖3 小时前
Android 依赖注入框架详解
android
SUNxuetian3 小时前
【Android Studio】升级AGP-8.6.1,Find Usage对Method失效的处理方法!
android·ide·gradle·android studio·安卓
阿华的代码王国4 小时前
【Android】搭配安卓环境及设备连接
android·java
__water4 小时前
RHA《Unity兼容AndroidStudio打Apk包》
android·unity·jdk·游戏引擎·sdk·打包·androidstudio
一起搞IT吧6 小时前
相机Camera日志实例分析之五:相机Camx【萌拍闪光灯后置拍照】单帧流程日志详解
android·图像处理·数码相机
浩浩乎@7 小时前
【openGLES】安卓端EGL的使用
android
Kotlin上海用户组8 小时前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
zzq19969 小时前
Android framework 开发者模式下,如何修改动画过度模式
android
木叶丸9 小时前
Flutter 生命周期完全指南
android·flutter·ios