需求:在單一 TextView 中顯示**不同字型、顏色、大小、底線**的文字(如標題粗體、內容正常、關鍵字紅色)。Android 提供兩種主流方案:HTML 標籤 與 Spannable。
方案一:Html.fromHtml()(簡單、宣告式、支援有限標籤)
基本用法
String html = "<b>標題</b><br/><small>描述文字</small><br/><font color='#FF0000'>重要提醒</font>";
textView.setText(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)); // API 24+ 需指定模式
支援標籤對照表
| 標籤 | 效果 | 備註 |
<b>, <strong> | 粗體 | |
<i>, <em> | 斜體 | |
<u> | 底線 | |
<strike>, <del> | 刪除線 | |
<sup>, <sub> | 上/下標 | |
<big>, <small> | 放大/縮小 | 相對預設字體 |
<font color='#RRGGBB'> | 文字顏色 | 支援 6/8 位十六進位 |
<font face='sans-serif-medium'> | 字體家族 | 系統字體名稱 |
<br/> | 換行 | |
<p> | 段落 | 含預設邊距 |
<a href='...'> | 超連結 | 需搭配 LinkMovementMethod |
<img src='...'> | 圖片 | 需實作 ImageGetter |
超連結可點擊完整範例
String html = "請閱讀 <a href='https://example.com/terms'>服務條款</a> 與 <a href='https://example.com/privacy'>隱私權政策</a>";
Spanned spanned = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
textView.setText(spanned);
textView.setMovementMethod(LinkMovementMethod.getInstance()); // 關鍵:啟用點擊
textView.setLinkTextColor(Color.BLUE); // 連結顏色
方案二:SpannableString / SpannableStringBuilder(程式化、功能完整、效能佳)
基本建構
SpannableStringBuilder builder = new SpannableStringBuilder();
// 1. 粗體標題
String title = "標題\n";
builder.append(title);
builder.setSpan(new StyleSpan(Typeface.BOLD), 0, title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 2. 正常描述
String desc = "描述文字\n";
int start = builder.length();
builder.append(desc);
builder.setSpan(new RelativeSizeSpan(0.9f), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 3. 紅色關鍵字
String warn = "重要提醒";
start = builder.length();
builder.append(warn);
builder.setSpan(new ForegroundColorSpan(Color.RED), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new StyleSpan(Typeface.BOLD), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(builder);
常用 Span 類別
| Span 類別 | 功能 | 建構範例 |
ForegroundColorSpan | 文字顏色 | new ForegroundColorSpan(Color.RED) |
BackgroundColorSpan | 背景顏色 | new BackgroundColorSpan(Color.YELLOW) |
StyleSpan | 粗體/斜體 | new StyleSpan(Typeface.BOLD) |
RelativeSizeSpan | 相對字體大小 | new RelativeSizeSpan(1.2f) |
AbsoluteSizeSpan | 絕對字體大小 (sp) | new AbsoluteSizeSpan(16, true) |
TypefaceSpan | 字體家族 | new TypefaceSpan("monospace") |
UnderlineSpan | 底線 | new UnderlineSpan() |
StrikethroughSpan | 刪除線 | new StrikethroughSpan() |
ClickableSpan | 可點擊區域 | 需覆寫 onClick(View) |
URLSpan | 超連結 | new URLSpan("https://...") |
ImageSpan | 插入圖片 | new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE) |
SubscriptSpan/SuperscriptSpan | 下/上標 | |
BulletSpan | 項目符號 | new BulletSpan(20, Color.GRAY) |
QuoteSpan | 引用縮排 | new QuoteSpan(Color.GRAY) |
ClickableSpan 完整可點擊範例
String text = "同意 服務條款 與 隱私權政策";
SpannableString spannable = new SpannableString(text);
spannable.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
// 開啟條款頁面
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
ds.setColor(Color.BLUE);
ds.setUnderlineText(true);
}
}, 2, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // "服務條款" 位置
spannable.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
// 開啟隱私權頁面
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
ds.setColor(Color.BLUE);
ds.setUnderlineText(true);
}
}, 9, 15, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // "隱私權政策" 位置
textView.setText(spannable);
textView.setMovementMethod(LinkMovementMethod.getInstance()); // 必須設定
textView.setHighlightColor(Color.TRANSPARENT); // 移除點擊高亮
方案選擇建議
| 場景 | 推薦方案 | 理由 |
| 簡單富文字、內容來自後端/設定檔、需支援超連結/圖片 | Html.fromHtml | 宣告式、易維護、標準 HTML 子集 |
| 高效能、動態組合、需精確控制每個字元屬性、自訂 Span 邏輯 | SpannableStringBuilder | 程式化、無解析開銷、功能最完整 |
| RecyclerView 大量項目、頻繁更新 | SpannableStringBuilder | 避免重複 HTML 解析、可快取 Spanned 物件 |
| 國際化字串資源 (strings.xml) | Html.fromHtml | XML 中直接寫 <b> 標籤,翻譯友善 |
效能提示
- HTML 解析有 CPU 開銷,大量列表請**快取 Spanned 結果**或改用 Spannable。
SpannableString 內容不可變,SpannableStringBuilder 可變,建構時用 Builder,最後可轉 Spanned 快取。
- API 24+
Html.fromHtml 預設行為變更,舊專案務必加 Html.FROM_HTML_MODE_LEGACY 旗標。
靈活運用兩種方案,能以最少程式碼實現豐富的文字排版效果。