スパンは強力なマークアップ オブジェクトで、文字単位や段落単位でテキストのスタイルを設定できます。スパンをテキスト オブジェクトにアタッチすると、色の追加、テキストのクリック可能化、文字サイズの拡大縮小、テキストのカスタム描画など、さまざまな方法でテキストを変更できます。また、スパンを使用すると、TextPaint
プロパティの変更、Canvas
への描画、テキスト レイアウトの変更も行えます。
Android には、さまざまなタイプのスパンが用意されており、一般的なテキスト スタイル パターンを幅広くカバーします。また、独自のスパンを作成して、カスタム スタイルを適用することもできます。
スパンを作成して適用する
スパンを作成するには、次の表にリスト表示されているいずれかのクラスを使用します。クラスは、テキスト自体が変更可能かどうか、テキスト マークアップが変更可能かどうか、スパンデータを格納する基盤データ構造がどのような構造か、という点において異なります。
クラス | テキストは変更可能か? | マークアップは変更可能か? | どのようなデータ構造か? |
---|---|---|---|
SpannedString |
× | × | リニア配列 |
SpannableString |
× | ○ | リニア配列 |
SpannableStringBuilder |
○ | ○ | 区間ツリー |
3 つのクラスはすべて、Spanned
インターフェースを拡張します。また、SpannableString
と SpannableStringBuilder
の場合は、Spannable
インターフェースも拡張します。
どちらを使用するかを決定する方法は次のとおりです。
- テキストやマークアップを作成後に変更しない場合は、
SpannedString
を使用します。 - 単一のテキスト オブジェクトに少数のスパンをアタッチし、テキスト自体は読み取り専用にする場合は、
SpannableString
を使用します。 - 作成後にテキストを変更する必要があり、テキストにスパンをアタッチする必要がある場合は、
SpannableStringBuilder
を使用します。 - テキスト オブジェクトに多数のスパンをアタッチする必要がある場合は、テキスト自体を読み取り専用にするかどうかにかかわらず、
SpannableStringBuilder
を使用します。
スパンを適用するには、Spannable
オブジェクトに対して setSpan(Object _what_, int _start_, int _end_, int
_flags_)
を呼び出します。what パラメータは、テキストに適用するスパンを参照します。start パラメータと end パラメータは、スパンを適用するテキストの部分を示します。
スパン境界の内側にテキストを挿入すると、スパンは自動的に拡張して、挿入テキストを含むようになります。スパン境界(start インデックスまたは end インデックス)にテキストを挿入する場合は、flags パラメータによって、スパンを拡張して挿入テキストを含めるかどうかを決定します。挿入テキストを含める場合は 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 );

ForegroundColorSpan
を使用してスタイルを設定したテキスト。このスパンは 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)");

Spannable.SPAN_EXCLUSIVE_INCLUSIVE
を使用しているため、追加テキストを含むように拡張するスパン1 つのテキストに複数のスパンをアタッチできます。太字で赤色のテキストを作成する例を以下に示します。
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)
)を使用したテキスト。Android のスパンタイプ
Android は、android.text.style パッケージ内に 20 種以上のスパンタイプを用意しています。Android では、次の 2 つの基準に基づいて、スパンを大まかに分類しています。
- スパンがテキストに及ぼす影響: スパンは、テキストの外観やテキストのサイズに影響を及ぼすことができます。
- スパンの対象範囲: 個々の文字単位で適用できるスパンと、段落単位の適用が必要となるスパンがあります。

以降のセクションでは、各カテゴリについて詳しく説明します。
テキストの外観に影響するスパン
文字単位で適用される一部のスパンは、テキスト色や背景色の変更、下線や取り消し線の追加など、テキストの外観に影響します。これらのスパンは 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
を使用して下線を引いたテキスト。
テキストの外観だけに影響するスパンは、レイアウトの再計算をトリガーせずに、テキストの再描画をトリガーします。このタイプのスパンは、UpdateAppearance
を実装し、CharacterStyle
を拡張します。CharacterStyle
サブクラスは、TextPaint
を更新するためのアクセスを提供することにより、テキストの描画方法を定義します。
テキストのサイズに影響するスパン
文字単位で適用される他のスパンは、行の高さや文字サイズなどのテキストのサイズに影響します。このタイプのスパンは、MetricAffectingSpan
クラスを拡張します。
次のコードサンプルは、テキストサイズを 50% 増加させる RelativeSizeSpan
を作成します。
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
を使用してテキストを大きくしました。
テキストのサイズに影響するスパンを適用すると、正しいレイアウトとレンダリングを実現するために、モニタリング オブジェクトがテキストを再測定します。そのため、たとえば、文字サイズを変更すると、異なる行に単語が表示される場合があります。上記のスパンを適用すると、再測定、テキスト レイアウトの再計算、テキストの再描画がトリガーされます。
テキストのサイズに影響するスパンは、MetricAffectingSpan
クラスを拡張します。この抽象クラスは、TextPaint
へのアクセスを提供することにより、スパンがテキスト測定に及ぼす影響をサブクラスで定義できるようにします。MetricAffectingSpan
は CharacterStyle
を拡張するため、サブクラスは文字単位でテキストの外観に影響します。
段落に影響するスパン
スパンは、テキスト ブロックの配置や余白の変更など、段落単位でテキストに影響を及ぼすこともできます。段落全体に影響するスパンは、ParagraphStyle
を実装します。このタイプのスパンを使用するには、末尾の改行文字を除く段落全体にアタッチします。段落スパンを段落全体以外に適用しようとしても、そのスパンは適用されません。
Android がテキスト内で段落を分離する方法を図 8 に示します。

\n
)で終了します。
次のコード例では、QuoteSpan
を段落に適用します。段落の開始位置や終了位置以外の位置にスパンをアタッチした場合、そのスタイルは適用されません。
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
。カスタムスパンを作成する
Android の既存のスパンで実現できる機能よりも高度な機能が必要な場合は、カスタムスパンを実装します。独自のスパンを実装する場合は、影響を受けるテキストが文字単位なのか段落単位なのか、さらに、影響を受けるのがテキストのレイアウトなのか外観なのか、を決定する必要があります。これにより、どの基本クラスを拡張し、どのインターフェースを実装する必要があるのかを判断できます。次の表を参考にしてください。
シナリオ | クラスまたはインターフェース |
---|---|
文字単位でテキストに影響するスパンの場合。 | CharacterStyle |
テキストの外観に影響するスパンの場合。 | UpdateAppearance |
テキストのサイズに影響するスパンの場合。 | UpdateLayout |
段落単位でテキストに影響するスパンの場合。 | ParagraphStyle |
たとえば、テキストのサイズと色を変更するカスタムスパンを実装する必要がある場合は、RelativeSizeSpan
を拡張します。RelativeSizeSpan
は継承によって CharacterStyle
を拡張し、2 つの Update
インターフェースを実装します。このクラスはすでに updateDrawState
と updateMeasureState
用のコールバックを備えているため、このコールバックをオーバーライドすることで、カスタム動作を実装できます。次のコードは、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); } }
この例は、カスタムスパンの作成方法を示しています。RelativeSizeSpan
と ForegroundColorSpan
をテキストに適用した場合も、同じ効果を実現できます。
スパンの使用方法をテストする
Spanned
インターフェースを使用すると、スパンを設定することも、テキストからスパンを取得することもできます。テストを行う場合は、Android JUnit テストを実装して、正しい位置に正しいスパンが追加されているか検証します。Text Styling サンプルアプリには、テキストに 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 をご覧ください。
カスタムスパンをテストする
スパンをテストする際は、想定どおりの変更内容が TextPaint
に組み込まれているか、Canvas
上に正しい要素が表示されるか検証します。たとえば、一部のテキストの先頭に箇条書きを付加するカスタムスパン実装があるとします。箇条書きはサイズと色が指定されており、ドローアブル領域の左マージンと箇条書きの間には隙間が存在します。
このクラスの動作をテストするには、AndroidJUnit テストを実装して、以下の点をチェックします。
- スパンを正しく適用した場合、指定したサイズと色の箇条書きがキャンバス上に表示され、左マージンと箇条書きの間に適切なスペースが存在します。
- スパンを適用しなかった場合、カスタム動作は表示されません。
これらのテストの実装は、GitHub の TextStyling サンプルで確認できます。
キャンバスのインタラクションをテストするには、キャンバスをモックし、モックしたオブジェクトを drawLeadingMargin()
メソッドに渡して、正しいメソッドが正しいパラメータで呼び出されるかどうかを検証します。
他のスパンテストのサンプルについては、BulletPointSpanTest をご覧ください。
スパンの使用方法に関するベスト プラクティス
TextView
内のテキストを設定する際、メモリ効率に優れた方法がいくつかあります。ニーズに応じて採用してください。
基盤テキストを変更せずにスパンのアタッチやデタッチを行う
TextView.setText()
には、スパンを異なる方法で処理する複数のオーバーロードが含まれています。たとえば、次のコードを使用することで、Spannable
テキスト オブジェクトを設定できます。
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
この setText()
のオーバーロードを呼び出すと、TextView
は、Spannable
のコピーを SpannedString
として作成し、メモリ内に CharSequence
として保持します。つまり、テキストとスパンは変更不可であるため、テキストやスパンを更新する必要がある場合は、新しい 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
オブジェクトに可変マークアップと不変テキストが含まれるようになります。スパンを更新するには、テキストを Spannable
として取得して、必要に応じてスパンを更新します。
スパンのアタッチやデタッチ、再配置を行うと、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()
をオーバーライドした後、新しい Factory
を使用するように TextView
に指示します。
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
TextView
への参照を取得したらすぐに、Spannable.Factory
オブジェクトを一度設定します。RecyclerView
を使用している場合は、ビューを最初にインフレートしたときに Factory
オブジェクトを設定します。これにより、RecyclerView
が新しいアイテムを ViewHolder
にバインドする際に、余分なオブジェクトが作成されるのを防ぐことができます。
内部スパン属性を変更する
カスタム箇条書きスパンの行頭記号の色など、可変スパンの内部属性だけを変更する必要がある場合は、スパンへの参照を作成時に保持することにより、setText()
を複数回呼び出すことによるオーバーヘッドを避けることができます。スパンを変更する必要がある場合は、参照を変更してから、変更した属性のタイプに応じて 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 には、スパンの処理を簡単にする拡張関数も含まれています。詳細については、androidx.core.text パッケージのドキュメントをご覧ください。