Span

試用 Compose
Jetpack Compose 是 Android 推薦的 UI 工具包。瞭解如何在 Compose 中使用文字。

Span 是功能強大的標記物件,可以用來設定文字樣式 字元或段落層級將 Span 附加至文字物件後, 新增文字、為文字加上可點擊屬性等 以自訂方式調整文字大小及繪製文字Span 也可以 變更 TextPaint 屬性,在 Canvas,並變更文字版面配置。

Android 提供數種類型的範圍,涵蓋各種常見文字 樣式模式您也可以建立專屬 Span 以套用自訂樣式。

建立與套用 Span

您可以使用下表所列的其中一個類別來建立 Span。 類別會根據文字本身是否可變動、文字是否可變動 標記可變動,且基礎資料結構包含時距資料。

類別 可變動文字 可變動標記 資料結構
SpannedString 線性陣列
SpannableString 線性陣列
SpannableStringBuilder 區間樹

這三個類別都會擴充 Spanned 存取 APISpannableStringSpannableStringBuilder 也會擴充 Spannable 介面。

以下說明如何決定要使用哪一種:

  • 如果您在建立文字或標記後不會予以修改,請使用 SpannedString
  • 如果您需要在單一文字物件中附加少量 Span,並且 文字本身為唯讀狀態,請使用 SpannableString
  • 建立後如需修改文字,且需要將 Span 附加到 請在文字中使用 SpannableStringBuilder
  • 如果您需要在文字物件中附加大量 Span, 關於文字本身是否為唯讀狀態,請使用 SpannableStringBuilder

如要套用 Span,請在 Spannable 物件上呼叫 setSpan(Object _what_, int _start_, int _end_, int _flags_)what 參數是指您的時距 用於文字內容,startend 參數會指出 並指定要套用 Span 的字詞。

如果您在 Span 的邊界內插入文字,Span 會自動擴展為 包含插入的文字在 Span「的」插入文字時 邊界,也就是位於 startend 索引,即 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
);
顯示灰色文字、部分紅色的圖片。
圖 1. 文字樣式: ForegroundColorSpan

由於 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_EXCLUSIVE_INCLUSIVE 時 Span 如何包含更多文字的圖片。
圖 2. 範圍擴大以包含 使用模型時 Spannable.SPAN_EXCLUSIVE_INCLUSIVE

您可以將多個 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
);
顯示具有多個跨距的文字的圖片:`ForegroundColorSpan(Color.RED)` 和 `StyleSpan(BOLD)`
圖 3. 包含多個 Span 的文字: 「ForegroundColorSpan(Color.RED)」和 StyleSpan(BOLD)

Android Span 類型

Android 在 android.text.style 套件中提供超過 20 種 Span 類型。Android 主要以兩種方式來分類 Span:

  • Span 如何影響文字:Span 會影響文字外觀或文字 指標。
  • 橫跨範圍:某些跨距可套用至個別字元,其他的跨距 都必須套用至整個段落。
,瞭解如何調查及移除這項存取權。
顯示不同時距類別的圖片
圖 4.Android 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);
這張圖片顯示如何使用「UnderlineSpan」為文字加上底線
圖 5. 文字會加上 UnderlineSpan

僅影響文字外觀的 Span 會在沒有的情況下觸發重新繪製文字 觸發重新計算版面配置。這類 Span 會 UpdateAppearance 和擴充內容 CharacterStyleCharacterStyle 子類別提供以下功能的存取權,可定義如何繪製文字: 更新 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);
顯示使用 RelativeSizeSpan 的圖片
圖 6. 使用 RelativeSizeSpan

套用會影響文字指標的 Span 會導致觀察物件 重新測量文字,進行正確的版面配置和轉譯作業,例如將 文字大小可能會導致字詞出現在不同的行。將上述的 Span 會觸發重新測量、重新計算文字版面配置以及重新繪製 文字。

影響文字指標的 Span 會擴充 MetricAffectingSpan 類別, 可讓子類別定義 Span 如何影響文字測量的抽象類別 取得 TextPaint 的存取權自 MetricAffectingSpan 年起 CharacterSpan,子類別會影響字元處的文字外觀 第二,自訂角色只能 套用至專案或機構

會影響段落的 Span

Span 也會影響段落層級的文字,例如改變 對齊或文字區塊邊界影響整個文字段落的 Span 會實作 ParagraphStyle。目的地: 請使用這些跨距,請將區段附加到整個段落中,但不包括結尾 換行字元如果您嘗試將段落 Span 套用至 整個段落,Android 完全不套用 Span。

圖 8 說明 Android 如何分隔文字中的段落。

圖 7.在 Android 中,段落會以 換行字元 (\n) 字元。

以下程式碼範例會將 按 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);
顯示 quoteSpan 範例的圖片
圖 8. QuoteSpan 自動套用到特定段落

建立自訂 Span

如果您需要的功能超過現有 Android Span 所能提供的功能,您可以實作自訂 Span。實作自己的 Span 時 而 Span 會影響字元層級或段落層級的文字 是否影響文字的版面配置或外觀。這麼做有利於 判斷可以擴充哪些基本類別,以及可能需要哪些介面。 以便實作。請參考下表:

情境 類別或介面
您的 Span 會影響半型字元層級的文字。 CharacterStyle
您的 Span 會影響文字外觀。 UpdateAppearance
您的 Span 會影響文字指標。 UpdateLayout
您的 Span 會影響段落層級的文字。 ParagraphStyle

舉例來說,如果您實作自訂 Span 以修改文字大小和 顏色,擴充 RelativeSizeSpan。透過繼承,RelativeSizeSpan 擴充 CharacterStyle 並實作兩個 Update 介面。自此 類別已提供 updateDrawStateupdateMeasureState 的回呼。 您可以覆寫這些回呼來實作自訂行為。 下列程式碼會建立自訂 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。您也可以達成 對文字套用 RelativeSizeSpanForegroundColorSpan

測試 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,並多次設定文字。變更者: 無論您是否設定 BufferTypeTextView 都會建立 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 套件的文件。