Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách set màu cho một phần text (Span) trong TextView Android

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_*)

FlagMô tả
SPAN_EXCLUSIVE_EXCLUSIVEMặc định. Text chèn vào đầu/cuối KHÔNG mang span.
SPAN_INCLUSIVE_EXCLUSIVEText chèn vào ĐẦU mang span, CUỐI không.
SPAN_EXCLUSIVE_INCLUSIVEText chèn vào ĐẦU không, CUỐI mang span.
SPAN_INCLUSIVE_INCLUSIVEText chèn vào ĐẦU và CUỐI đều mang span.
SPAN_POINT_POINTGiố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() cho ClickableSpan/URLSpan mới hoạt động.
  • Index trong setSpan: start inclusive, end exclusive (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").