Span 是功能強大的標記物件,可以用來設定文字樣式
字元或段落層級將 Span 附加至文字物件後,
新增文字、為文字加上可點擊屬性等
以自訂方式調整文字大小及繪製文字Span 也可以
變更 TextPaint
屬性,在
Canvas
,並變更文字版面配置。
Android 提供數種類型的範圍,涵蓋各種常見文字 樣式模式您也可以建立專屬 Span 以套用自訂樣式。
建立與套用 Span
您可以使用下表所列的其中一個類別來建立 Span。 類別會根據文字本身是否可變動、文字是否可變動 標記可變動,且基礎資料結構包含時距資料。
類別 | 可變動文字 | 可變動標記 | 資料結構 |
---|---|---|---|
SpannedString |
否 | 否 | 線性陣列 |
SpannableString |
否 | 是 | 線性陣列 |
SpannableStringBuilder |
是 | 是 | 區間樹 |
這三個類別都會擴充 Spanned
存取 APISpannableString
和 SpannableStringBuilder
也會擴充 Spannable
介面。
以下說明如何決定要使用哪一種:
- 如果您在建立文字或標記後不會予以修改,請使用
SpannedString
。 - 如果您需要在單一文字物件中附加少量 Span,並且
文字本身為唯讀狀態,請使用
SpannableString
。 - 建立後如需修改文字,且需要將 Span 附加到
請在文字中使用
SpannableStringBuilder
。 - 如果您需要在文字物件中附加大量 Span,
關於文字本身是否為唯讀狀態,請使用
SpannableStringBuilder
。
如要套用 Span,請在 Spannable
物件上呼叫 setSpan(Object _what_, int _start_, int _end_, int
_flags_)
。what 參數是指您的時距
用於文字內容,start 和 end 參數會指出
並指定要套用 Span 的字詞。
如果您在 Span 的邊界內插入文字,Span 會自動擴展為
包含插入的文字在 Span「的」插入文字時
邊界,也就是位於 start 或 end 索引,即 flag
參數可決定 Span 是否會擴展以包含插入的文字。使用
這個
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
敬上
標記以加入插入的文字,然後使用
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
。
排除插入的文字。
以下範例說明如何附加
將 ForegroundColorSpan
轉換為
字串:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE )
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE );
由於 Span 是使用 Spannable.SPAN_EXCLUSIVE_INCLUSIVE
設定,因此時距
會展開以在跨距邊界納入插入的文字,如
範例:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) spannable.insert(12, "(& fon)")
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ); spannable.insert(12, "(& fon)");
您可以將多個 Span 附加至同一段文字。以下範例說明 建立粗體和紅色的文字:
Kotlin
val spannable = SpannableString("Text is spantastic!") spannable.setSpan(ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan( StyleSpan(Typeface.BOLD), 8, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE )
Java
SpannableString spannable = new SpannableString("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); spannable.setSpan( new StyleSpan(Typeface.BOLD), 8, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
Android Span 類型
Android 在 android.text.style 套件中提供超過 20 種 Span 類型。Android 主要以兩種方式來分類 Span:
- Span 如何影響文字:Span 會影響文字外觀或文字 指標。
- 橫跨範圍:某些跨距可套用至個別字元,其他的跨距 都必須套用至整個段落。
以下各節將詳細說明這些類別。
會影響文字外觀的 Span
部分在字元層級套用的 Span 會影響文字外觀,例如
變更文字或背景顏色,以及加上底線或刪除線。這些
Span 會延伸
CharacterStyle
類別。
以下程式碼範例說明如何將 UnderlineSpan
套用至底線
例如:
Kotlin
val string = SpannableString("Text with underline span") string.setSpan(UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with underline span"); string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
僅影響文字外觀的 Span 會在沒有的情況下觸發重新繪製文字
觸發重新計算版面配置。這類 Span 會
UpdateAppearance
和擴充內容
CharacterStyle
。
CharacterStyle
子類別提供以下功能的存取權,可定義如何繪製文字:
更新 TextPaint
。
會影響文字指標的 Span
其他在字元層級套用的跨距會影響文字指標,例如線條
包括高度和文字大小這類跨距可以擴充
MetricAffectingSpan
敬上
類別
以下程式碼範例會建立
RelativeSizeSpan
將文字大小增加 50%:
Kotlin
val string = SpannableString("Text with relative size span") string.setSpan(RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with relative size span"); string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
套用會影響文字指標的 Span 會導致觀察物件 重新測量文字,進行正確的版面配置和轉譯作業,例如將 文字大小可能會導致字詞出現在不同的行。將上述的 Span 會觸發重新測量、重新計算文字版面配置以及重新繪製 文字。
影響文字指標的 Span 會擴充 MetricAffectingSpan
類別,
可讓子類別定義 Span 如何影響文字測量的抽象類別
取得 TextPaint
的存取權自 MetricAffectingSpan
年起
CharacterSpan
,子類別會影響字元處的文字外觀
第二,自訂角色只能
套用至專案或機構
會影響段落的 Span
Span 也會影響段落層級的文字,例如改變
對齊或文字區塊邊界影響整個文字段落的 Span 會實作 ParagraphStyle
。目的地:
請使用這些跨距,請將區段附加到整個段落中,但不包括結尾
換行字元如果您嘗試將段落 Span 套用至
整個段落,Android 完全不套用 Span。
圖 8 說明 Android 如何分隔文字中的段落。
以下程式碼範例會將
按 QuoteSpan
可翻個段落。請注意,
如果您將 Span 附加至開頭或結尾以外的任何位置
段落,Android 完全不會套用此樣式。
Kotlin
spannable.setSpan(QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
spannable.setSpan(new QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
建立自訂 Span
如果您需要的功能超過現有 Android Span 所能提供的功能,您可以實作自訂 Span。實作自己的 Span 時 而 Span 會影響字元層級或段落層級的文字 是否影響文字的版面配置或外觀。這麼做有利於 判斷可以擴充哪些基本類別,以及可能需要哪些介面。 以便實作。請參考下表:
情境 | 類別或介面 |
---|---|
您的 Span 會影響半型字元層級的文字。 | CharacterStyle |
您的 Span 會影響文字外觀。 | UpdateAppearance |
您的 Span 會影響文字指標。 | UpdateLayout |
您的 Span 會影響段落層級的文字。 | ParagraphStyle |
舉例來說,如果您實作自訂 Span 以修改文字大小和
顏色,擴充 RelativeSizeSpan
。透過繼承,RelativeSizeSpan
擴充 CharacterStyle
並實作兩個 Update
介面。自此
類別已提供 updateDrawState
和 updateMeasureState
的回呼。
您可以覆寫這些回呼來實作自訂行為。
下列程式碼會建立自訂 Span,擴充 RelativeSizeSpan
和
會覆寫 updateDrawState
回呼以設定 TextPaint
的顏色:
Kotlin
class RelativeSizeColorSpan( size: Float, @ColorInt private val color: Int ) : RelativeSizeSpan(size) { override fun updateDrawState(textPaint: TextPaint) { super.updateDrawState(textPaint) textPaint.color = color } }
Java
public class RelativeSizeColorSpan extends RelativeSizeSpan { private int color; public RelativeSizeColorSpan(float spanSize, int spanColor) { super(spanSize); color = spanColor; } @Override public void updateDrawState(TextPaint textPaint) { super.updateDrawState(textPaint); textPaint.setColor(color); } }
這個範例說明如何建立自訂 Span。您也可以達成
對文字套用 RelativeSizeSpan
和 ForegroundColorSpan
。
測試 Span 用法
Spanned
介面可讓您設定時距,並從中擷取時距
文字。測試時,實作 Android JUnit
test,確認是否新增正確的 Span
放在正確的位置文字樣式範例
應用程式
包含 Span,並透過附加的方式將標記套用至項目符號
文字的 BulletPointSpan
。以下程式碼範例顯示如何測試
項目符號是否正常顯示:
Kotlin
@Test fun textWithBulletPoints() { val result = builder.markdownToSpans("Points\n* one\n+ two") // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()) // Get all the spans attached to the SpannedString. val spans = result.getSpans<Any>(0, result.length, Any::class.java) // Check whether the correct number of spans are created. assertEquals(2, spans.size.toLong()) // Check whether the spans are instances of BulletPointSpan. val bulletSpan1 = spans[0] as BulletPointSpan val bulletSpan2 = spans[1] as BulletPointSpan // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1).toLong()) assertEquals(11, result.getSpanEnd(bulletSpan1).toLong()) assertEquals(11, result.getSpanStart(bulletSpan2).toLong()) assertEquals(14, result.getSpanEnd(bulletSpan2).toLong()) }
Java
@Test public void textWithBulletPoints() { SpannedString result = builder.markdownToSpans("Points\n* one\n+ two"); // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()); // Get all the spans attached to the SpannedString. Object[] spans = result.getSpans(0, result.length(), Object.class); // Check whether the correct number of spans are created. assertEquals(2, spans.length); // Check whether the spans are instances of BulletPointSpan. BulletPointSpan bulletSpan1 = (BulletPointSpan) spans[0]; BulletPointSpan bulletSpan2 = (BulletPointSpan) spans[1]; // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1)); assertEquals(11, result.getSpanEnd(bulletSpan1)); assertEquals(11, result.getSpanStart(bulletSpan2)); assertEquals(14, result.getSpanEnd(bulletSpan2)); }
如需更多測試範例,請參閱: GitHub 上的 MarkdownBuilderTest。
測試自訂 Span
測試 Span 時,請確認 TextPaint
包含預期內的
並在 Canvas
上顯示正確的元素。例如,假設要實作一個在部分文字前加上項目符號的自訂 Span。項目符號已指定大小和顏色,而且兩者之間有間隔
繪製區域左側邊界和項目符號之間
您可以實作 AndroidJUnit 測試來檢查這個類別的行為,以確認下列事項:
- 如果正確套用時距,系統會用項目符號將指定大小 顏色出現在畫布,左側之間有適當的空間 和項目符號
- 如果您未套用 Span,則不會顯示任何自訂行為。
您可以在 TextStyling 查看這些測試的實作結果 範例。
您可以模擬畫布並通過模擬,測試 Canvas 互動
新增至
drawLeadingMargin()
敬上
方法,並確認使用正確的方法呼叫正確的方法
參數。
您可以在 BulletPointSpanTest。
使用 Span 的最佳做法
在 TextView
中設定文字的方法有很多種,具體取決於
按需求調整
在不變更基礎文字的情況下附加或移除 Span
TextView.setText()
敬上
包含多個超載,能以不同方式處理 Span。舉例來說:
請使用下列程式碼設定 Spannable
文字物件:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
在呼叫此 setText()
重載時,TextView
會將 Spannable
的副本建立為 SpannedString
,並以 CharSequence
形式儲存在記憶體中。這表示文字和 Span 無法變更,因此在您需要
更新文字或 Span,建立新的 Spannable
物件並呼叫
會再次觸發 setText()
,這樣也會觸發系統重新評估及重新繪製
版面配置。
如要表示時距必須可變動,您可以改用
setText(CharSequence text, TextView.BufferType
type)
、
如以下範例所示:
Kotlin
textView.setText(spannable, BufferType.SPANNABLE) val spannableText = textView.text as Spannable spannableText.setSpan( ForegroundColorSpan(color), 8, spannableText.length, SPAN_INCLUSIVE_INCLUSIVE )
Java
textView.setText(spannable, BufferType.SPANNABLE); Spannable spannableText = (Spannable) textView.getText(); spannableText.setSpan( new ForegroundColorSpan(color), 8, spannableText.getLength(), SPAN_INCLUSIVE_INCLUSIVE);
在這個範例中,
BufferType.SPANNABLE
敬上
參數會使 TextView
建立 SpannableString
,而
TextView
保留的 CharSequence
物件現在包含可變動的標記,
不可變更的文字如要更新 Span,請將文字擷取為 Spannable
,然後
視需要更新 Span。
附加、移除 Span 或調整 Span 位置時,TextView
會自動更新,以反映文字的變更。如果變更內部屬性
現有跨距,呼叫 invalidate()
則可進行外觀相關變更,或
requestLayout()
,即可進行指標相關變更。
在 TextView 中多次設定文字
在某些情況下,例如使用
RecyclerView.ViewHolder
、
您可能會想重複使用 TextView
,並多次設定文字。變更者:
無論您是否設定 BufferType
,TextView
都會建立
CharSequence
物件的副本,並將該物件保留在記憶體中。如此一來
TextView
會主動更新,您無法更新原始更新
用於更新文字的 CharSequence
物件。這代表您每次設定新的
文字,TextView
會建立新物件。
如要進一步控管這項程序,並避免加入多餘的物件
您可以建立
Spannable.Factory
並覆寫
newSpannable()
。
與其建立新的文字物件,您可以投放並傳回現有的
CharSequence
做為 Spannable
,如以下範例所示:
Kotlin
val spannableFactory = object : Spannable.Factory() { override fun newSpannable(source: CharSequence?): Spannable { return source as Spannable } }
Java
Spannable.Factory spannableFactory = new Spannable.Factory(){ @Override public Spannable newSpannable(CharSequence source) { return (Spannable) source; } };
符合以下條件時,就必須使用 textView.setText(spannableObject, BufferType.SPANNABLE)
以及設定文字否則,來源 CharSequence
會建立為 Spanned
無法轉換為 Spannable
,導致 newSpannable()
擲回
ClassCastException
。
覆寫 newSpannable()
後,指示 TextView
使用新的 Factory
:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
取得對Spannable.Factory
TextView
。如果您使用 RecyclerView
,請在以下時間設定 Factory
物件:
才算是衝高觀看次數這樣就能避免在
「RecyclerView
」會將新項目繫結至「ViewHolder
」。
變更內部 Span 屬性
您只需變更可變動時距的內部屬性 (例如
自訂項目符號範圍中的項目符號顏色,可以避免呼叫負擔而造成負擔
在建立跨距時保留參照,即可多次執行 setText()
。
需要修改 Span 時,您可以修改參照,然後呼叫
TextView
上的 invalidate()
或 requestLayout()
,視類型而定
屬性。
在以下程式碼範例中,自訂項目符號實作方式包含 使用者輕觸按鈕時,預設顏色會變成灰色:
Kotlin
class MainActivity : AppCompatActivity() { // Keeping the span as a field. val bulletSpan = BulletPointSpan(color = Color.RED) override fun onCreate(savedInstanceState: Bundle?) { ... val spannable = SpannableString("Text is spantastic") // Setting the span to the bulletSpan field. spannable.setSpan( bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) styledText.setText(spannable) button.setOnClickListener { // Change the color of the mutable span. bulletSpan.color = Color.GRAY // Color doesn't change until invalidate is called. styledText.invalidate() } } }
Java
public class MainActivity extends AppCompatActivity { private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED); @Override protected void onCreate(Bundle savedInstanceState) { ... SpannableString spannable = new SpannableString("Text is spantastic"); // Setting the span to the bulletSpan field. spannable.setSpan(bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE); styledText.setText(spannable); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // Change the color of the mutable span. bulletSpan.setColor(Color.GRAY); // Color doesn't change until invalidate is called. styledText.invalidate(); } }); } }
使用 Android KTX 擴充功能函式
Android KTX 也包含擴充功能函式,可以處理 Span 讓您更容易詳情請參閱 androidx.core.text 套件的文件。