In the last article, we introduced how to elegantly encapsulate anonymous inner classes (DSLS, higher-order functions) in Kotlin, and I went into some detail on how to use DSLS in Kotlin. This article can be viewed as an exercise in DSLS from the previous article.
The source come from
Spannable realizes rich text display in Android development, which is also a relatively common use scenario, such as displaying privacy Policy and Service Agreement on the login page. Usually, this is a Span with custom colors and click events, and the following code is roughly needed to use it:
private fun agreePrivate(a) {
val tv = findViewById<TextView>(R.id.tv_agree)
val builder = SpannableStringBuilder()
val text = "I have read and agree to the Privacy Policy."
builder.append(text)
// Set span click events
val clickableSpan = object :ClickableSpan(){
override fun onClick(widget: View) {
//do some thing
}
}
builder.setSpan(clickableSpan, 9.15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// Set span without underscores
val noUnderlineSpan = NoUnderlineSpan()
builder.setSpan(noUnderlineSpan, 9.15, Spanned.SPAN_MARK_MARK)
// Set the span text color
val foregroundColorSpan = ForegroundColorSpan(Color.parseColor("#0099FF"))
builder.setSpan(foregroundColorSpan, 9.15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// The Settings are clickable
tv.movementMethod = LinkMovementMethod.getInstance()
tv.setText(builder)
}
class NoUnderlineSpan : UnderlineSpan() {
override fun updateDrawState(ds: TextPaint) {
ds.color = ds.linkColor
ds.isUnderlineText = false}}Copy the code
It’s a bit of a hassle to use, as the code above has three setSpans for just one span, and if you need to use a span in many places, the code looks really unelegant. Is there a more elegant way? The answer is DSL, and the above code is finally wrapped in DSL as follows:
tvTestDsl.buildSpannableString {
addText("I have read and agreed.")
addText("Privacy Policy"){
setColor("#0099FF")
onClick(false) {
//do some thing}}}Copy the code
Their display is exactly the same, and there is no doubt that the DSL approach is more elegant and convenient for the caller.
Implementation idea:
When I encapsulated the idea of Spannable with a DSL, the first thing I wrote was how I would use it, and I scribbled the code above on a piece of paper.
- It should be an extension function of TextView
- Inside it is DSL-style code
- Each paragraph of text has a function that sets the color and click events
So there are two interfaces and extension functions as follows:
interface DslSpannableStringBuilder {
// Add a paragraph
fun addText(text: String, method: (DslSpanBuilder. () - >Unit)? = null)
}
interface DslSpanBuilder {
// Set the text color
fun setColor(color: String)
// Set the click event
fun onClick(useUnderLine: Boolean = true, onClick: (View) - >Unit)
}
// Create an extension function for TextView that takes the extension function of the interface
fun TextView.buildSpannableString(init: DslSpannableStringBuilder. () - >Unit) {
// Concrete implementation class
val spanStringBuilderImpl = DslSpannableStringBuilderImpl()
spanStringBuilderImpl.init()
movementMethod = LinkMovementMethod.getInstance()
// Return SpannableStringBuilder via the implementation class
text = spanStringBuilderImpl.build()
}
Copy the code
In the last article, we said that in a DSL-style function, the argument should be an extension function of an interface (or its implementation class), so we are essentially restricting the functions that can be called in the DSL through the interface. The last article used the implementation class, this article uses the interface, the reason is very simple, the above is to extend the original interface into DSL style, this article is directly from nothing, the implementation of DSL style.
Implement the corresponding interface:
In fact, like me for the first time out of the DSL novice, thinking is the most difficult, with the interface, DSL hierarchy, the rest is relatively simple implementation. Look directly at the code:
class DslSpannableStringBuilderImpl : DslSpannableStringBuilder {
private val builder = SpannableStringBuilder()
// Record the last index value since the last text was added
var lastIndex: Int = 0
var isClickable = false
override fun addText(text: String, method: (DslSpanBuilder. () - >Unit)? {
val start = lastIndex
builder.append(text)
lastIndex += text.length
valspanBuilder = DslSpanBuilderImpl() method? .let { spanBuilder.it() } spanBuilder.apply { onClickSpan? .let { builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) isClickable =true
}
if(! useUnderLine) {valnoUnderlineSpan = NoUnderlineSpan() builder.setSpan(noUnderlineSpan, start, lastIndex, Spanned.SPAN_MARK_MARK) } foregroundColorSpan? .let { builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } }fun build(a): 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}}Copy the code
conclusion
To use a DSL, you need to create the interface of the function you want to use in the DSL, and then declare the function parameter as the extension function of that interface.
If you have a nest like mine in your DSL, you need to create an interface for the nested call for that nest (the nesting in this article is intentional, but using a single interface to pass arguments can also do the trick).