Giới thiệu
Android cho phép style một phần text trong TextView bằng Spannable (SpannableString, SpannableStringBuilder). Dùng các Span để đổi màu, size, font, underline, clickable, image span, v.v. Bài viết tập trung vào ForegroundColorSpan và các span hữu ích khác.
Các bước thực hiện
Bước 1: Tạo dự án mới
Android Studio → File → New Project → Empty View Activity.
Bước 2: Thiết kế giao diện (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvSpannable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:lineSpacingExtra="8dp"
android:gravity="center"
tools:text="Text với nhiều màu sắc" />
<Button
android:id="@+id/btnApplySpans"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Áp dụng Spannable" />
</LinearLayout>
Bước 3: MainActivity.kt - Các loại Span phổ biến
package com.example.spannabledemo
import android.graphics.Color
import android.graphics.Typeface
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.*
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.spannabledemo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnApplySpans.setOnClickListener { applySpans() }
}
private fun applySpans() {
val text = "Tôi yêu lập trình Android rất nhiều!"
val spannable = SpannableStringBuilder(text)
// 1. ForegroundColorSpan - Đổi màu chữ
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(this, R.color.purple_200)),
3, 6, // "yêu" (index 3-5)
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
// 2. BackgroundColorSpan - Màu nền chữ
spannable.setSpan(
BackgroundColorSpan(Color.YELLOW),
7, 17, // "lập trình"
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
// 3. RelativeSizeSpan - Thay đổi size tương đối
spannable.setSpan(
RelativeSizeSpan(1.5f), // 1.5x size gốc
18, 25, // "Android"
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
// 4. StyleSpan - Bold, Italic, Bold Italic
spannable.setSpan(
StyleSpan(Typeface.BOLD_ITALIC),
26, 31, // "rất nhiều"
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
// 5. UnderlineSpan - Gạch chân
spannable.setSpan(
UnderlineSpan(),
0, 2, // "Tôi"
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
// 6. StrikethroughSpan - Gạch ngang
spannable.setSpan(
StrikethroughSpan(),
31, 32, // "!"
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
// 7. ClickableSpan - Xử lý click trên phần text
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
Toast.makeText(this@MainActivity, "Đã click vào 'Android'!", Toast.LENGTH_SHORT).show()
}
override fun updateDrawState(ds: android.text.TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false // Bỏ underline mặc định của link
ds.color = ContextCompat.getColor(this@MainActivity, R.color.purple_700)
}
}
spannable.setSpan(clickableSpan, 18, 25, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// 8. ImageSpan - Chèn icon vào text
// val drawable = ContextCompat.getDrawable(this, R.drawable.ic_android)?.apply { setBounds(0, 0, 48, 48) }
// spannable.setSpan(ImageSpan(drawable!!, ImageSpan.ALIGN_BOTTOM), 18, 18, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// 9. URLSpan - Link tự động mở browser
// spannable.setSpan(URLSpan("https://developer.android.com"), 18, 25, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// Áp dụng vào TextView
binding.tvSpannable.text = spannable
// QUAN TRỌNG: Phải setMovementMethod để ClickableSpan hoạt động
binding.tvSpannable.movementMethod = android.text.method.LinkMovementMethod.getInstance()
}
}
Spannable Flags (SPAN_*)
| Flag | Mô tả |
|---|---|
SPAN_EXCLUSIVE_EXCLUSIVE | Mặc định. Text chèn vào đầu/cuối KHÔNG mang span. |
SPAN_INCLUSIVE_EXCLUSIVE | Text chèn vào ĐẦU mang span, CUỐI không. |
SPAN_EXCLUSIVE_INCLUSIVE | Text chèn vào ĐẦU không, CUỐI mang span. |
SPAN_INCLUSIVE_INCLUSIVE | Text chèn vào ĐẦU và CUỐI đều mang span. |
SPAN_POINT_POINT | Giống EXCLUSIVE_EXCLUSIVE nhưng cho zero-length span. |
Extension Function hữu ích (Kotlin)
// Extensions.kt
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.*
fun String.spannable(): SpannableStringBuilder = SpannableStringBuilder(this)
fun SpannableStringBuilder.color(@ColorInt color: Int, start: Int, end: Int, flags: Int = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE): SpannableStringBuilder {
setSpan(ForegroundColorSpan(color), start, end, flags)
return this
}
fun SpannableStringBuilder.backgroundColor(@ColorInt color: Int, start: Int, end: Int, flags: Int = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE): SpannableStringBuilder {
setSpan(BackgroundColorSpan(color), start, end, flags)
return this
}
fun SpannableStringBuilder.bold(start: Int, end: Int, flags: Int = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE): SpannableStringBuilder {
setSpan(StyleSpan(Typeface.BOLD), start, end, flags)
return this
}
fun SpannableStringBuilder.italic(start: Int, end: Int, flags: Int = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE): SpannableStringBuilder {
setSpan(StyleSpan(Typeface.ITALIC), start, end, flags)
return this
}
fun SpannableStringBuilder.size(proportion: Float, start: Int, end: Int, flags: Int = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE): SpannableStringBuilder {
setSpan(RelativeSizeSpan(proportion), start, end, flags)
return this
}
fun SpannableStringBuilder.clickable(start: Int, end: Int, onClick: (View) -> Unit, flags: Int = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE): SpannableStringBuilder {
setSpan(object : ClickableSpan() {
override fun onClick(widget: View) = onClick(widget)
override fun updateDrawState(ds: android.text.TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
}, start, end, flags)
return this
}
Sử dụng:
val spannable = "Hello World Android".spannable()
.color(Color.RED, 6, 11) // "World" đỏ
.bold(12, 19) // "Android" bold
.size(1.5f, 12, 19) // "Android" to hơn
.clickable(12, 19) { toast("Clicked Android!") }
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()
HTML.fromHtml() - Cách đơn giản hơn (API 24+)
Nếu chỉ cần basic styling (bold, italic, color, link), dùng HTML:
val htmlText = "Tôi yêu lập trình Android!"
textView.text = Html.fromHtml(htmlText, Html.FROM_HTML_MODE_LEGACY)
// Hoặc API 24+:
textView.text = Html.fromHtml(htmlText, Html.FROM_HTML_OPTION_USE_CSS_COLORS)
- Ưu: Code ngắn, dễ đọc, hỗ trợ CSS color.
- Nhược: Hạn chế span types, performance kém hơn Spannable cho text lớn/thay đổi thường xuyên.
Jetpack Compose: AnnotatedString
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Color.Red)) { append("Red ") }
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { append("Bold ") }
withStyle(style = SpanStyle(fontSize = 24.sp)) { append("Big") }
append(" Normal")
}
)
Lưu ý quan trọng
- Luôn set
movementMethod = LinkMovementMethod.getInstance()choClickableSpan/URLSpanmới hoạt động. - Index trong setSpan:
startinclusive,endexclusive (giống substring). SpannableString= immutable (không sửa text được),SpannableStringBuilder= mutable (khuyên dùng).- Span overlap: Span sau ghi đè span trước (nếu cùng loại).
- Performance: Tránh tạo Spannable mới mỗi frame (trong onDraw/animation). Cache nếu có thể.
- Accessibility: TalkBack đọc span bình thường, trừ ClickableSpan (có hint "double tap to activate").