ในการพัฒนาแอปบนแอนดรอยด์ก็อาจจะต้องทำให้ TextView สามารถกดเพื่อทำอะไรบางอย่างได้ และในบางครั้งก็อาจจะต้องทำให้ผู้ใช้กดที่บางส่วนของข้อความใน TextView ได้เท่านั้นด้วย

ยกตัวอย่างเช่นข้อความแบบนี้

ดังนั้นการใช้ setOnClickListener จึงไม่เหมาะกับกรณีนี้ซักเท่าไร เนื่องจากต้องการให้กดได้แค่เฉพาะข้อความที่ต้องการเท่านั้น ไม่ใช่ข้อความทั้งหมด จึงต้องใช้ SpannableString เข้ามาช่วยแก้ปัญหาดังกล่าวแทน

สำหรับข้อความบางส่วนที่กดได้ เจ้าของบล็อกจะเรียกว่า Link Text

เกี่ยวกับ SpannableString

SpannableString เป็นคลาสที่ช่วยให้นักพัฒนาใส่ Markup ให้กับข้อความเพื่อนำไปใช้กับ UI อย่าง TextView ได้ และจะเรียก Markup ใด ๆ ที่กำหนดให้กับข้อความใน SpannableString ว่า Span

ซึ่ง Span ที่มีให้ใช้งานจะมีหลายแบบ แต่ในกรณีนี้เจ้าของบล็อกจะใช้ ClickableSpan เพื่อทำ Link Text นั่นเอง

และเพื่อให้นำไปใช้งานได้ง่าย เจ้าของบล็อกจึงไม่ได้ใช้งาน ClickableSpan โดยตรง แต่จะนำไปสร้างเป็นคลาสขึ้นมาเองเพื่อให้ใช้งานได้ง่ายขึ้น

class TextClickableSpan(
    private val url: String?,
    val onTextClick: (view: View, url: String?) -> Unit
) : ClickableSpan() {
    override fun onClick(view: View) {
        onTextClick(view, url)
    }
}

จากตัวอย่างของ TextClickableSpan จะเห็นว่ามีการรับ Parameter ที่ชื่อว่า url เป็น String เข้ามาด้วย เพื่อช่วยให้เขียนโค้ดได้สะดวกขึ้นเท่านั้น

ผู้ที่หลงเข้ามาอ่านสามารถเปลี่ยน URL String ให้เป็นข้อมูลแบบอื่นได้ตามใจชอบ

และจากตัวอย่างคำว่า "All you want is over here" ถ้าใช้กับ TextClickableSpan ที่เจ้าของบล็อกสร้างขึ้นมา ก็จะใช้คำสั่งประมาณนี้

val text = "All you want is over here"
val url = "https://google.com"
val start = 16
val end = 24
val onTextClickSpan = TextClickableSpan(url) { _, url ->
    // Do something
}
val spannableString = SpannableString(text).apply {
    setSpan(onTextClickSpan, start, end, 0)
}

val textView: TextView = /* ... */
textView.text = spannableString

โดย Index ที่ 16 ถึง 24 คือตำแหน่งของคำว่า "over here" นั่นเอง

อย่าลืมกำหนด Movement Method ให้กับ TextView ด้วย

เดิมที TextView ไม่ได้กำหนดให้รองรับ Link Text จึงต้องเพิ่มคำสั่งให้กับ TextView ก่อนเสมอ

val textView: TextView = /* ... */
textView.movementMethod = LinkMovementMethod()

และถ้าอยากเปลี่ยนสีเฉพาะข้อความดังกล่าว ก็ให้ใช้คำสั่ง setLinkTextColor ได้เลย

เพื่อให้การทำ Link Text นั้นยืดหยุ่นต่อการนำไปใช้งาน นักพัฒนาควรแยกข้อความที่ต้องการทำ Link Text ออกจากข้อความหลัก แล้วแทนที่ด้วย Keyword เพื่อใช้ในการคำนวณในภายหลัง

โดยแนะนำให้สร้าง Data Class เพื่อเก็บข้อมูลสำหรับคำที่ต้องการทำ Link Text

data class LinkText(
    val keyword: String,
    val text: String,
    val url: String
)

สมมติว่าข้อความและ Link Text ที่ต้องการแทรกเข้าไปในข้อความมีลักษณะแบบนี้

val message = "See {link1} and {link2} for more information"
val linkTexts = listOf(
    LinkText("{link1}", "Google", "https://google.com"),
    LinkText("{link2}", "Akexorcist", "https://akexorcist.dev")
)

เพื่อให้ข้อมูลนำไปคำนวณและใช้งานได้ง่ายขึ้น เจ้าของบล็อกจึงสร้าง Data Class เพิ่มขึ้นมาอีก 2 ตัว

data class MergeSpanValue(
    val message: String,
    val spanValues: List<SpanValue>
)

data class SpanValue(
    val start: Int,
    val end: Int,
    val url: String
)

และใช้วิธีการคำนวณเพื่อเอาค่าใน Link Text ไปแทนที่ข้อความใน message แล้วคำนวณหา start กับ end เพื่อใช้ตอนกำหนด Span ให้กับ SpannableString แบบนี้

// Initiate
val message: String = /* ... */
val linkTexts: List<LinkText> = /* ... */

// Calculate
val initial = MergeSpanValue(
    message = message,
    spanValues = listOf()
)
val result = linkTexts
    .sortedBy { linkText -> message.indexOf(linkText.keyword) }
    .fold(initial) { acc: MergeSpanValue, linkText: LinkText ->
        val start = acc.message.indexOf(linkText.keyword)
        val end = start + linkText.text.count()
        val replacedMessage = acc.message.replace(linkText.keyword, linkText.text)
        acc.copy(
            message = replacedMessage,
            spanValues = acc.spanValues + SpanValue(
                start = start,
                end = end,
                url = linkText.url
            )
        )
    }

val spannableString = SpannableString(result.message).apply {
    result.spanValues.forEach { value ->
        val onClick = TextClickableSpan(value.url) { _, url ->
            // Do something
        }
        setSpan(onClick, value.start, value.end, 0)
    }
}

// Assign
val textView: TextView = /* ... */
textView.movementMethod = LinkMovementMethod()
textView.text = spannableString

เพียงเท่านี้ก็จะได้ Link Text หลายตัวในข้อความเดียวกันแล้ว

แน่นอนว่าวิธีการคำนวณดังกล่าวจะต้องกำหนด Keyword แต่ละตัวให้ไม่เหมือนกัน และ Keyword ที่อยู่ในข้อความก็ต้องห้ามซ้ำกันด้วย ดังนั้นถ้าต้องการนำไปใช้ในลักษณะที่แตกต่างกันออกไป ก็สามารถแก้ไขวิธีการคำนวณได้ตามใจชอบเลย