探索 Android 中的 Span

插圖由 Virginia Poltrack(https://twitter.com/VPoltrack) 繪制

探索 Android 中的 Span

在 Android 中,使用 Span 定義文本的樣式. 通過(guò) Span 改變幾個(gè)文字的顏色,讓它們可點(diǎn)擊笛钝,放縮文字的大小甚至是繪制自定義的項(xiàng)目符號(hào)點(diǎn)(bullet points,國(guó)外人名中名字之間的間隔符號(hào) · 愕宋,HTML 中無(wú)序列表項(xiàng)的默認(rèn)符號(hào))玻靡。Span 能夠改變 TextPaint 屬性,在 Canvas 上繪制中贝,甚至是改變文本的布局和影響像行高這樣的元素囤捻。Span 是可以附加到文本或者從本文分離的標(biāo)記對(duì)象(markup objects);它們可以被應(yīng)用到部分或整段的文本中雄妥。

讓我們來(lái)看看Span如何使用最蕾、提供了哪些開(kāi)箱即用的功能依溯、怎樣簡(jiǎn)單地創(chuàng)建我們自己的 Span 以及如何使用和測(cè)試它們老厌。

Styling text in Android

Creating custom spans

Testing custom spans implementation

Testing spans usage

在 Android 上定義文本樣式

Android 提供了幾種定義文本樣式的方法:

  • 單一樣式 —— 樣式應(yīng)用在 TextView 顯示的整個(gè)文本
  • 多重樣式 —— 多種樣式應(yīng)用在字符或者段落級(jí)別的文本

單一樣式 使用 XML 屬性或者 樣式和主題 引入了 TextView 的所有內(nèi)容的樣式瘟则。這種方式實(shí)現(xiàn)簡(jiǎn)單,通過(guò) XML 即可實(shí)現(xiàn)枝秤,但是并不能只定義部分內(nèi)容的樣式醋拧。舉個(gè)例子,通過(guò)設(shè)置 textStyle=”bold”淀弹,所有的文本都會(huì)變?yōu)楹隗w丹壕;你不能只定義特定的幾個(gè)字符為黑體。

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="32sp"
    android:textStyle="bold"/>

多重樣式 引入了給一段文本添加多種樣式的功能薇溃。例如菌赖,一個(gè)單詞斜體而另一個(gè)粗體。多重樣式可以通過(guò)使用 HTML 標(biāo)簽沐序、 Span 或者是在 Canvas 上處理自定義的文本繪制琉用。

左圖:?jiǎn)我粯邮轿谋尽TO(shè)置了 textSize=”32sp” 和 textStyle=”bold” 的 TextView 策幼。右圖:多重樣式文本邑时。設(shè)置了 ForegroundColorSpan, StyleSpan(ITALIC), ScaleXSpan(1.5f), StrikethroughSpan 的文本。

左圖:?jiǎn)我粯邮轿谋咎亟恪TO(shè)置了 textSize=”32sp” 和 textStyle=”bold” 的 TextView 晶丘。右圖:多重樣式文本。設(shè)置了 ForegroundColorSpan, StyleSpan(ITALIC), ScaleXSpan(1.5f), StrikethroughSpan 的文本唐含。

HTML 標(biāo)簽是解決簡(jiǎn)單問(wèn)題的簡(jiǎn)單辦法浅浮,例如使文本加粗、斜體捷枯,甚至是顯示項(xiàng)目符號(hào)點(diǎn)脑题。為了展示含有 HTML 標(biāo)簽的文本,使用 Html.fromHtml 方法铜靶。在內(nèi)部實(shí)現(xiàn)時(shí)叔遂,HTML 標(biāo)簽被轉(zhuǎn)換成了 span 。但是請(qǐng)注意争剿,Html 類(lèi)并不支持完整的 HTML 標(biāo)簽和 CSS 樣式已艰,例如將小黑點(diǎn)改為其他的顏色。

val text = "My text <ul><li>bullet one</li><li>bullet two</li></ul>"
myTextView.text = Html.fromHtml(text)

當(dāng)你有文本樣式的需求蚕苇,但是 Android 平臺(tái)默認(rèn)不支持時(shí)哩掺,你還可以手動(dòng)地在 Canvas 上繪制文本,例如讓文字彎曲排布涩笤。

Span 允許你實(shí)現(xiàn)具有更細(xì)粒度自定義的多重樣式文本嚼吞。舉個(gè)例子盒件,通過(guò) BulletSpan,你可以定義你的段落文本擁有項(xiàng)目符號(hào)點(diǎn)舱禽。你可以定制文本和點(diǎn)號(hào)之間的間距和點(diǎn)號(hào)的顏色炒刁。從 Android P 開(kāi)始,你甚至可以 設(shè)置點(diǎn)號(hào)的半徑 誊稚。你也可以創(chuàng)建 span 的自定義實(shí)現(xiàn)翔始。在文章中查看 “創(chuàng)建自定義 span” 部分可以找到如何實(shí)現(xiàn)。

val spannable = SpannableString("My text \nbullet one\nbullet two")
spannable.setSpan(
    BulletPointSpan(gapWidthPx, accentColor),
    /* 起始索引 */ 9, /* 終止索引 */ 18,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(
     BulletPointSpan(gapWidthPx, accentColor),
     /* 起始索引 */ 20, /* 終止索引 */ spannable.length,
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
myTextView.text = spannable
左圖:使用 HTML 標(biāo)簽里伯;中圖:使用 BulletSpan城瞎,默認(rèn)圓點(diǎn)大小疾瓮;右圖:在 Android P 上使用 BulletSpan 或者自定義實(shí)現(xiàn)脖镀。

左圖:使用 HTML 標(biāo)簽;中圖:使用 BulletSpan狼电,默認(rèn)圓點(diǎn)大醒鸦摇;右圖:在 Android P 上使用 BulletSpan 或者自定義實(shí)現(xiàn)漫萄。

你可以組合使用單一樣式和多重樣式卷员。你可以考慮將設(shè)置給 TextView 的樣式作為一種“基本”樣式,而 span 文本樣式是應(yīng)用在基本樣式“之上”并且會(huì)覆蓋基本樣式的樣式腾务。例如毕骡,當(dāng)給一個(gè) TextView 設(shè)置了 textColor=”@color.blue” 屬性且給頭4個(gè)字符應(yīng)用了 ForegroundColorSpan(Color.PINK),則頭4個(gè)字符會(huì)使用 span 設(shè)置的粉色岩瘦,而其他文本使用 TextView 屬性設(shè)置的顏色未巫。

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textColor="@color/blue"/>
val spannable = SpannableString(“Text styling”)
spannable.setSpan(
     ForegroundColorSpan(Color.PINK), 
     0, 4, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
myTextView.text = spannable
TextView 組合使用 XML 屬性和 span 樣式

TextView 組合使用 XML 屬性和 span 樣式

應(yīng)用 span

當(dāng)使用 span 時(shí),你會(huì)和以下類(lèi)的其中之一打交道:SpannedString, SpannableStringSpannableStringBuilder启昧。 它們之間的區(qū)別在于文本或標(biāo)記對(duì)象是可改變的還是不可改變的以及它們使用的內(nèi)部結(jié)構(gòu):SpannedStringSpannableString 使用線(xiàn)性數(shù)組記錄已添加的 span叙凡,而 SpannableStringBuilder 使用 區(qū)間樹(shù)

下面是如何決定使用哪一個(gè)的方法:

  • 讀取而不設(shè)置 文本和 span密末? --> SpannedString
  • 設(shè)置文本和 span握爷? --> SpannableStringBuilder
  • 設(shè)置 少量的 span (<~ 10)? --> SpannableString
  • 設(shè)置 大量的 span (>~ 10)严里? --> SpannableStringBuilder

舉個(gè)例子新啼,你用到的文本并不會(huì)改變,但你想要附加 span 時(shí)刹碾,你應(yīng)該使用 SpannableString燥撞。


║     類(lèi)                 ║    可變文本   ║     可變標(biāo)記    ║
═════════════════════════════════════════════════════════
║ SpannedString          ║      否      ║       否       ║
║ SpannableString        ║      否      ║       是       ║
║ SpannableStringBuilder ║      是      ║       是       ║

上面所有的這些類(lèi)都繼承自 Spanned 接口,但擁有可變標(biāo)記的類(lèi)( SpannableStringSpannableStringBuilder) 同時(shí)也繼承自 Spannable

Spanned --> 帶有不可變標(biāo)記的不可變文本

Spannable (繼承自 Spanned) --> 帶有可變標(biāo)記的不可變文本

通過(guò) Spannable 對(duì)象調(diào)用 setSpan(Object what, int start, int end, int flags) 方法應(yīng)用 span物舒。 What 對(duì)象是一個(gè)標(biāo)記色洞,應(yīng)用于從開(kāi)始到結(jié)束的索引之間的文本。falg 標(biāo)志位標(biāo)記了 span 是否應(yīng)該擴(kuò)展至包含插入文本的開(kāi)始和結(jié)束的點(diǎn)冠胯。任何標(biāo)志位設(shè)置以后火诸,只要插入文本的位置位于開(kāi)始位置和結(jié)束位置之間,span 就會(huì)自動(dòng)的擴(kuò)展涵叮。

舉個(gè)例子惭蹂,設(shè)置 ForegroundColorSpan 可以像下面這樣完成:

val spannable = SpannableStringBuilder(“Text is spantastic!”)
spannable.setSpan(
     ForegroundColorSpan(Color.RED), 
     8, 12, 
     Spannable.SPAN_EXCLUSIVE_INCLUSIVE)

因?yàn)?span 設(shè)置時(shí)使用了 SPAN_EXCLUSIVE_INCLUSIVE 標(biāo)志位伞插,在 span 的后面插入文本時(shí)割粮,新插入的文本也會(huì)自動(dòng)地繼承此 span。

val spannable = SpannableStringBuilder(“Text is spantastic!”)
spannable.setSpan(
     ForegroundColorSpan(Color.RED), 
     /* start index */ 8, /* end index */ 12, 
     Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
spannable.insert(12, “(& fon)”)
左圖:帶有 ForegroundColorSpan 的文本媚污;右圖:帶有 ForegroundColorSpan 和 Spannable.SPAN_EXCLUSIVE_INCLUSIVE 的文本舀瓢。

左圖:帶有 ForegroundColorSpan 的文本;右圖:帶有 ForegroundColorSpan 和 Spannable.SPAN_EXCLUSIVE_INCLUSIVE 的文本耗美。

如果 span 設(shè)置了 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 標(biāo)志位京髓,在 span 后面插入文本時(shí)則不會(huì)修改 span 的結(jié)束索引。

多個(gè) span 可以被組合且同時(shí)附加到同一段文本上商架。例如堰怨,粗體紅色的文本可以像這樣構(gòu)建:

val spannable = SpannableString(“Text is spantastic!”)
spannable.setSpan(
     ForegroundColorSpan(Color.RED), 
     8, 12, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(
     StyleSpan(BOLD), 
     8, spannable.length, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
帶有多種 span 的文本:ForegroundColorSpan(Color.RED) 和 StyleSpan(BOLD)

帶有多種 span 的文本:ForegroundColorSpan(Color.RED) 和 StyleSpan(BOLD)

Framework span

Android framework 定義了幾個(gè)接口和抽象類(lèi),它們會(huì)在測(cè)量和渲染時(shí)被檢查蛇摸。這些類(lèi)有允許 span 訪(fǎng)問(wèn)像 TextPaintCanvas 對(duì)象的方法备图。

Android framework 在 android.text.style 包、主要接口的字類(lèi)和抽象類(lèi)中提供了20+的 span赶袄, 我們可以通過(guò)下面幾個(gè)方式對(duì) span 進(jìn)行分類(lèi):

  • 基于 span 是否只改變外形或者 text 的大小或布局
  • 基于 是否會(huì)在字符或段落級(jí)別影響文本
span 類(lèi)別:字符對(duì)比段落揽涮,外形對(duì)比大小

span 類(lèi)別:字符對(duì)比段落,外形對(duì)比大小

影響外形對(duì)比影響尺寸的 span

第一種類(lèi)型以修改外形的方式在字符級(jí)別起作用:文本或背景顏色饿肺、下劃線(xiàn)蒋困、中橫線(xiàn)等等,它會(huì)觸發(fā)文本重新繪制但是并不會(huì)重新布局敬辣。這些 span 引入了 UpdateAppearance 且繼承自 CharacterStyle. CharacterStyle 字類(lèi)通過(guò)提供更新 TextPaint 的訪(fǎng)問(wèn)方法雪标,定義了怎樣繪制文本。

影響外形的 span

影響外形的 span

影響尺寸的 span 更改了文本的尺寸和布局溉跃,因此觀(guān)察 span 的變化的對(duì)象會(huì)重新繪制文本以保證布局和渲染的正確村刨。

舉個(gè)例子,影響文本字體大小的 span 要求重新測(cè)量和布局喊积,也要求重新繪制烹困。這種 span 通常繼承自 MetricAffectingSpan 類(lèi)。這個(gè)抽象類(lèi)通過(guò)提供對(duì) TextPaint 的訪(fǎng)問(wèn)乾吻,允許字類(lèi)定義 span 如何影響文本測(cè)量髓梅,而 MetricAffectingSpan 繼承自 CharacterSpan拟蜻,子類(lèi)在字符級(jí)別影響文本的外形。

影響尺寸的 span

影響尺寸的 span

你可能會(huì)想要一直重新創(chuàng)建帶有文本和標(biāo)記的 CharSequence 并且調(diào)用 TextView.setText(CharSequence) 方法枯饿,但是這樣做很有可能一直觸發(fā)已經(jīng)創(chuàng)建好的布局和額外的對(duì)象的重新測(cè)量和重新繪制酝锅。為了減少性能損耗,將文本設(shè)置為 TextView.setText(Spannable, BufferType.SPANNABLE)奢方,然后當(dāng)你需要更改 span 的時(shí)候搔扁,通過(guò)將 TextView.getText() 轉(zhuǎn)換為 Spannable 從 TextView 獲得 Spannable 對(duì)象。我們會(huì)在未來(lái)的文章中詳細(xì)討論 TextView.setText 的實(shí)現(xiàn)原理和不同的性能優(yōu)化方式蟋字。

舉個(gè)例子稿蹲,考慮通過(guò)這樣的方式設(shè)置和獲取 Spannable

val spannableString = SpannableString(“Spantastic text”)
// 將文本設(shè)置為一個(gè) Spannable
textView.setText(spannableString, BufferType.SPANNABLE)
// 然后獲取 TextView 持有的 text 對(duì)象引用
// 這里之所以能轉(zhuǎn)換為 Spannable 是因?yàn)槲覀冎皩⑺O(shè)置為了 BufferType.SPANNABLE
val spannableText = textView.text as Spannable

現(xiàn)在,當(dāng)我們?cè)?spannableText 上設(shè)置了 span 之后鹊奖,我們就不需要再調(diào)用 textView.setText 了苛聘,因?yàn)槲覀冋谥苯有薷?TextView 持有的 CharSequence 對(duì)象的引用。

這是當(dāng)我們?cè)O(shè)置了不同的 span 之后會(huì)發(fā)生什么:

情形1: 影響外觀(guān)的 span

spannableText.setSpan(
     ForegroundColorSpan(colorAccent), 
     0, 4, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

當(dāng)我們附加了一個(gè)影響外觀(guān)的 span 之后忠聚,TextView.onDraw 方法被調(diào)用但 TextView.onLayout 沒(méi)有设哗。這是文本重繪,但寬和高保持原樣两蟀。

情形2: 影響尺寸的 span

spannableText.setSpan(
     RelativeSizeSpan(2f), 
     0, 4, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

因?yàn)?RelativeSizeSpan 改變了文本的大小网梢,文本的寬和高變化,文本的布局方式(舉個(gè)例子赂毯,在 TextView 的大小沒(méi)有變化的情況下战虏,一個(gè)特定的單詞現(xiàn)在可能會(huì)換行)。TextView 需要計(jì)算新的大小所以 onMeasureonLayout 均被調(diào)用欢瞪。

左圖:ForegroundColorSpan——影響外觀(guān)的 span活烙;右圖:RelativeSizeSpan——影響尺寸的 span

左圖:ForegroundColorSpan——影響外觀(guān)的 span;右圖:RelativeSizeSpan——影響尺寸的 span

影響字符和影響段落的 span

一個(gè) span 對(duì)文本產(chǎn)生的影響既可以在字符級(jí)別遣鼓,更新元素啸盏,如背景顏色、樣式或大小骑祟,也可以在段落級(jí)別回懦,更改整個(gè)文本塊的對(duì)齊或者邊距。根據(jù)所需的樣式次企,span 既可以繼承自CharacterStyle怯晕,也可以引入 ParagraphStyle。 繼承自 ParagraphStyle 的 span 必須從第一個(gè)字符附加到單個(gè)段落的最后一個(gè)字符缸棵,否則 span 不會(huì)被顯示舟茶。在 Android 中,段落是基于換行符 (\n) 定義的。

在 Android 中吧凉,段落是基于換行符 (\n) 定義的隧出。

在 Android 中,段落是基于換行符 (\n) 定義的阀捅。

影響段落的 span

影響段落的 span

舉個(gè)例子胀瞪,像 BackgroundColorSpan 這樣的 CharacterStyle,可以被附加到文本中的任何字符上饲鄙。這里凄诞,我們把它附加到第五到第八個(gè)字符上。

val spannable = SpannableString(“Text is\nspantastic”)
spannable.setSpan(
    BackgroundColorSpan(color),
    5, 8,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

ParagraphStyle 的 span忍级,像 QuoteSpan帆谍,只能夠被附加到段落的開(kāi)始,否則行和文本之間的邊距就不會(huì)出現(xiàn)颤练。例如既忆,“Text is\nspantastic” 在文本的第8個(gè)字符包含了一個(gè)換行符驱负,所以我們可以給它附加一個(gè) QuoteSpan嗦玖,段落從那里開(kāi)始就會(huì)被添加樣式。如果我們?cè)?或8之外的任何位置附加 span跃脊,text 就不會(huì)被添加樣式宇挫。

spannable.setSpan(
    QuoteSpan(color), 
    8, text.length, 
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
左圖:BackgroundColorSpan -- 影響字符的 span。右圖:QuoteSpan -- 影響段落的 span

左圖:BackgroundColorSpan -- 影響字符的 span酪术。右圖:QuoteSpan -- 影響段落的 span

創(chuàng)建自定義 span

在實(shí)現(xiàn)你自己的 span 時(shí)器瘪,你需要確定你的 span 是否會(huì)影響字符或段落級(jí)別的文本,以及它是否也會(huì)影響文本的布局或外觀(guān)绘雁。但是橡疼,在從頭開(kāi)始編寫(xiě)自己的實(shí)現(xiàn)之前,檢查一下是否可以使用 framework 中提供的 span庐舟。

太長(zhǎng)不看:

  • 字符級(jí)別 影響文本 -> CharacterStyle
  • 段落級(jí)別 影響文本 -> ParagraphStyle
  • 影響 文本外觀(guān) -> UpdateAppearance
  • 影響 文本尺寸 -> UpdateLayout

假設(shè)我們需要實(shí)現(xiàn)一個(gè) span欣除,它允許以一定的比例增加文本的大小,比如 RelativeSizeSpan挪略,并設(shè)置文本的顏色历帚,比如 ForegroundColorSpan。為此杠娱,我們可以擴(kuò)展 RelativeSizeSpan挽牢,并且 RelativeSizeSpan 提供了 updateDrawStateupdateMeasureState 回調(diào),我們可以復(fù)寫(xiě)繪制狀態(tài)回調(diào)并設(shè)置 TextPaint 的顏色摊求。

class RelativeSizeColorSpan(
    @ColorInt private val color: Int,
    size: Float
) : RelativeSizeSpan(size) {
    override fun updateDrawState(textPaint: TextPaint?) {
         super.updateDrawState(ds)
         textPaint?.color = color
    }
}

注意:同樣的效果可以通過(guò)在同一文本上同時(shí)應(yīng)用 RelativeSizeSpanForegroundColorSpan 實(shí)現(xiàn)禽拔。

測(cè)試自定義 span 的實(shí)現(xiàn)

測(cè)試 span 意味著檢查確實(shí)已對(duì) TextPaint 進(jìn)行了預(yù)期的修改,或者是否已經(jīng)將正確的元素繪制到了 canvas 上。例如睹栖,假設(shè)一個(gè) span 的自定義實(shí)現(xiàn)為段落添加制定大小和顏色的項(xiàng)目符號(hào)點(diǎn)寥闪,以及左邊距和項(xiàng)目符號(hào)點(diǎn)之間的間隙。在 android-text sample 查看具體實(shí)現(xiàn)磨淌。為了測(cè)試這個(gè)類(lèi)疲憋,實(shí)現(xiàn)一個(gè) AndroidJUnit 類(lèi),確實(shí)檢查:

  • 一個(gè)特定大小的圓被繪制在了 canvas 上
  • 如果沒(méi)有被附加到文本上梁只,則沒(méi)有任何繪制
  • 基于構(gòu)造函數(shù)的參數(shù)缚柳,設(shè)置了正確的邊距

測(cè)試 Canvas 的交互可以通過(guò) mock canvas,給 drawLeadingMargin 方法傳 mock 過(guò)的引用并使用正確的參數(shù)驗(yàn)證是否已調(diào)用正確的方法來(lái)實(shí)現(xiàn)搪锣。

val canvas = mock(Canvas::class.java)
val paint = mock(Paint::class.java)
val text = SpannableString("text")

@Test fun drawLeadingMargin() {
    val x = 10
    val dir = 15
    val top = 5
    val bottom = 7
    val color = Color.RED
    // 給定一個(gè)已經(jīng)設(shè)置到文本上的 span
    val span = BulletPointSpan(GAP_WIDTH, color)
    text.setSpan(span, 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    // 當(dāng)前面的邊距已經(jīng)被繪制
    span.drawLeadingMargin(canvas, paint, x, dir, top, 0, bottom,
            text, 0, 0, true, mock(Layout::class.java))
    // 檢查確定的 canvas 和 paint 方法在確定的順序下被調(diào)用        
    val inOrder = inOrder(canvas, paint)
    // bullet point paint color is the one we set
    inOrder.verify(paint).color = color
    inOrder.verify(paint).style = eq<Paint.Style>(Paint.Style.FILL)
    // 一個(gè)確定大小的圓在確定位置被繪制
    val xCoordinate = GAP_WIDTH.toFloat() + x.toFloat()
    +dir * BulletPointSpan.DEFAULT_BULLET_RADIUS
    val yCoord = (top + bottom) / 2f
    inOrder.verify(canvas)
           .drawCircle(
                eq(xCoordinate),
                eq(yCoord), 
                eq(BulletPointSpan.DEFAULT_BULLET_RADIUS), 
                eq(paint))
    verify(canvas, never()).save()
    verify(canvas, never()).translate(
               eq(xCoordinate), 
               eq(yCoordinate))
}

BulletPointSpanTest 查看其余的測(cè)試秋忙。

測(cè)試 span 的用法

Spanned 接口允許給文本設(shè)置 span 和從文本獲取 span 。通過(guò)實(shí)現(xiàn)一個(gè) Android JUnit 測(cè)試构舟,檢查是否在正確的位置添加了正確的 span灰追。在 android-text sample 我們把項(xiàng)目符號(hào)點(diǎn)標(biāo)記標(biāo)簽轉(zhuǎn)換為了項(xiàng)目符號(hào)點(diǎn)。這是通過(guò)給文本在正確的位置附加 BulletPointSpans狗超。 下面展示了它是如何被測(cè)試的:

@Test fun textWithBulletPoints() {
val result = builder.markdownToSpans(“Points\n* one\n+ two”)
// 檢查標(biāo)記標(biāo)簽被移除
assertEquals(“Points\none\ntwo”, result.toString())
// 獲取所有附加到 SpannedString 上的 span
val spans = result.getSpans<Any>(0, result.length, Any::class.java)assertEquals(2, spans.size.toLong())
// 檢查 span 確實(shí)是 BulletPointSpan
val bulletSpan = spans[0] as BulletPointSpan
// 檢查開(kāi)始和結(jié)束索引正是期望值
assertEquals(7, result.getSpanStart(bulletSpan).toLong())
assertEquals(11, result.getSpanEnd(bulletSpan).toLong())
val bulletSpan2 = spans[1] as BulletPointSpan
assertEquals(11, result.getSpanStart(bulletSpan2).toLong())
assertEquals(14, result.getSpanEnd(bulletSpan2).toLong())
}

查看 MarkdownBuilderTest 獲取更多測(cè)試示例弹澎。

注意,如果你需要在測(cè)試之外遍歷 span努咐,使用 Spanned#nextSpanTransition 而不是 Spanned#getSpans苦蒿,因?yàn)樗阅芨谩?/p>


span 是一個(gè)非常強(qiáng)大的概念,它深深的嵌入在文本渲染功能中渗稍。它們可以訪(fǎng)問(wèn) TextPaint 和 Canvas 等組件佩迟,這些組件允許在 Android 上使用高度可自定義的文本樣式。在 Android P 中竿屹,我們?yōu)?framework span 添加了大量文檔报强,所以,在實(shí)現(xiàn)你自己的 span 之前拱燃,查看那些能夠獲取到的內(nèi)容秉溉。

在以后的文章中,我們將向你詳細(xì)介紹 span 在底層是如何工作的以及怎樣高效地使用它們扼雏。例如坚嗜,你需要使用 textView.setText(CharSequence, BufferType) 或者 Spannable.Factory。 有關(guān)原因的詳細(xì)信息诗充,請(qǐng)保持關(guān)注苍蔬。

非常感謝 Siyamed Sinir,Clara Bayarri 和 Nick Butcher.

本文原作者 Florina Muntenescu蝴蜓,Android Developer Advocate @Google . 原文地址:https://medium.com/google-developers/spantastic-text-styling-with-spans-17b0c16b4568. 本文由 TonnyL 翻譯碟绑,發(fā)表在: https://tonnyl.github.io/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末俺猿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子格仲,更是在濱河造成了極大的恐慌押袍,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凯肋,死亡現(xiàn)場(chǎng)離奇詭異谊惭,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)侮东,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)圈盔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人悄雅,你說(shuō)我怎么就攤上這事驱敲。” “怎么了宽闲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵众眨,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我容诬,道長(zhǎng)娩梨,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任放案,我火速辦了婚禮姚建,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吱殉。我一直安慰自己,他們只是感情好厘托,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布友雳。 她就那樣靜靜地躺著,像睡著了一般铅匹。 火紅的嫁衣襯著肌膚如雪押赊。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天包斑,我揣著相機(jī)與錄音流礁,去河邊找鬼。 笑死罗丰,一個(gè)胖子當(dāng)著我的面吹牛神帅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播萌抵,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼找御,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼元镀!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起霎桅,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤栖疑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后滔驶,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體遇革,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年揭糕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了澳淑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡插佛,死狀恐怖杠巡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情雇寇,我是刑警寧澤氢拥,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站锨侯,受9級(jí)特大地震影響嫩海,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜囚痴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一叁怪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧深滚,春花似錦奕谭、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至生兆,卻和暖如春难捌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鸦难。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工根吁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人合蔽。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓击敌,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親辈末。 傳聞我的和親對(duì)象是個(gè)殘疾皇子愚争,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,506評(píng)論 25 707
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線(xiàn)程映皆,因...
    小菜c閱讀 6,358評(píng)論 0 17
  • 原文鏈接:https://github.com/opendigg/awesome-github-android-u...
    IM魂影閱讀 32,901評(píng)論 6 472
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,708評(píng)論 22 664
  • 今天畫(huà)得很受打擊,第一次完成已經(jīng)上色轰枝,發(fā)現(xiàn)兩腿距離太遠(yuǎn)捅彻,姿勢(shì)特別難看。然后修改腿間距鞍陨,怎么插也留有痕跡步淹,而且,感覺(jué)...
    RoiceZ閱讀 320評(píng)論 2 5