上一篇文章我们介绍了如何在 kotlin 优雅的封装匿名内部类(DSL、高阶函数),其中我还算详细的介绍了在 Kotlin 中如何使用 DSL,本文可以看作是对上一篇文章中 DSL 的一个实战。
源码:junerver/SpannableStringDslExtension
欢迎 star & PR
源从何来
在 Android 开发中 Spannable 实现富文本显示,也算是一个比较常见的使用场景,例如在登录页显示《隐私政策》、《服务协议》,通常这是一个有自定义颜色与点击事件的 Span,使用起来大致需要写如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| private fun agreePrivate() { val tv = findViewById<TextView>(R.id.tv_agree) val builder = SpannableStringBuilder() val text = "我已详细阅读并同意《隐私政策》" builder.append(text) val clickableSpan = object :ClickableSpan(){ override fun onClick(widget: View) { } } builder.setSpan(clickableSpan, 9, 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) val noUnderlineSpan = NoUnderlineSpan() builder.setSpan(noUnderlineSpan, 9, 15, Spanned.SPAN_MARK_MARK) val foregroundColorSpan = ForegroundColorSpan(Color.parseColor("#0099FF")) builder.setSpan(foregroundColorSpan, 9, 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) tv.movementMethod = LinkMovementMethod.getInstance() tv.setText(builder) }
class NoUnderlineSpan : UnderlineSpan() { override fun updateDrawState(ds: TextPaint) { ds.color = ds.linkColor ds.isUnderlineText = false } }
|
用起来还是比较麻烦的,就像上面的代码只是一个 span 就写了三个 setSpan
,如果需要使用 Span 的地方比较多,这些代码看起来实在是不够优雅。有没有更优雅方式呢,答案就是 DSL,上面的代码最终通过 DSL 封装后如下:
1 2 3 4 5 6 7 8 9
| tvTestDsl.buildSpannableString { addText("我已详细阅读并同意") addText("《隐私政策》"){ setColor("#0099FF") onClick(false) { } } }
|
他们的显示效果是完全一致的,无疑 DSL 的方式更加优雅,对于调用者而言也更加方便。
实现思路:
当我有用 DSL 封装 Spannable 这个想法时,我首先写的是我应该如何去使用它,当时我在纸上胡乱的写下了上面的那段代码。
- 它应该是 TextView的一个扩展函数
- 它的内部是 DSL 风格的代码
- 它的每段文字都有设置颜色 & 点击事件的函数
所以就有了如下的两个接口与扩展函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| interface DslSpannableStringBuilder { fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null) }
interface DslSpanBuilder { fun setColor(color: String) fun onClick(useUnderLine: Boolean = true, onClick: (View) -> Unit) }
fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> Unit) { val spanStringBuilderImpl = DslSpannableStringBuilderImpl() spanStringBuilderImpl.init() movementMethod = LinkMovementMethod.getInstance() text = spanStringBuilderImpl.build() }
|
上一篇文章我们说了, 在 DSL 风格的函数中,其参数应当是某个接口(或者他的实现类)的扩展函数,这样我们相当于通过接口来限定了在 DSL 中可调用的函数。上一篇中使用的是实现类,本文中使用的是接口,原因很简单,上文是扩展原有接口变成 DSL 风格,本文是直接从无至有,实现的 DSL 风格。
实现相应接口:
其实对于像我这样初次接出 DSL 的新手而言,思路是最难的,有了接口,有了 DSL 层级,剩下的就是相对简单的实现了。直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| class DslSpannableStringBuilderImpl : DslSpannableStringBuilder { private val builder = SpannableStringBuilder() var lastIndex: Int = 0 var isClickable = false
override fun addText(text: String, method: (DslSpanBuilder.() -> Unit)?) { val start = lastIndex builder.append(text) lastIndex += text.length val spanBuilder = DslSpanBuilderImpl() method?.let { spanBuilder.it() } spanBuilder.apply { onClickSpan?.let { builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) isClickable = true } if (!useUnderLine) { val noUnderlineSpan = NoUnderlineSpan() builder.setSpan(noUnderlineSpan, start, lastIndex, Spanned.SPAN_MARK_MARK) } foregroundColorSpan?.let { builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } }
fun build(): SpannableStringBuilder { return builder } }
class DslSpanBuilderImpl : DslSpanBuilder { var foregroundColorSpan: ForegroundColorSpan? = null var onClickSpan: ClickableSpan? = null var useUnderLine = true
override fun setColor(color: String) { foregroundColorSpan = ForegroundColorSpan(Color.parseColor(color)) }
override fun onClick(useUnderLine: Boolean, onClick: (View) -> Unit) { onClickSpan = object : ClickableSpan() { override fun onClick(widget: View) { onClick(widget) } } this.useUnderLine = useUnderLine } }
class NoUnderlineSpan : UnderlineSpan() { override fun updateDrawState(ds: TextPaint) { ds.color = ds.linkColor ds.isUnderlineText = false } }
|
总结
想要使用 DSL 离不开接口与扩展函数,需要先创建想要在 DSL 中使用的函数的接口,然后声明函数参数为该接口的扩展函数。
如果 DSL 中存在像我这样的嵌套,那么就需要为这个嵌套再创建一个用于嵌套调用的接口(本文的嵌套是故意为之,使用单个接口传参也可以实现这样的效果)。