SpannableStringはandroid.textパッケージのAndroid SDKクラスで、TextView内の1つのテキスト文字列の異なる部分に複数のスタイルを適用できます。HTMLマークアップとは異なり、SpannableStringはSpanオブジェクトのレベルで動作し、テキストの視覚的表示(色、サイズ、書体、下線、インタラクティブ要素)を制御します。Google Developersによると、SpannableStringはAndroidシステムコンポーネントでリンクの書式設定に使用されています。サードパーティライブラリを使用せずにテキストをスタイル設定する主要な方法です。
重要なポイント
SpannableStringはSpannableインターフェースを実装するAndroidクラスで、視覚的表示を制御するSpanオブジェクトのセットとともにテキストを格納します。通常のStringとは異なり、SpannableStringは特定の文字範囲にスタイル属性を付加できます。テキストの一部を赤くしたり、見出しのフォントサイズを大きくしたり、段落内にクリック可能なリンクを追加したりできます。
このクラスはandroid.textパッケージにあり、API Level 1から利用可能です。SpannableStringは不変です。一度作成すると構造が固定され、テキストを置き換えるには新しいオブジェクトを作成する必要があります。動的編集には、スタイルを失わずに文字の挿入と削除をサポートするSpannableStringBuilderが使用されます。
CharSequenceはテキストデータの基本インターフェースで、String、StringBuilder、SpannableStringが実装しています。SpannableStringとStringの主な違いは、部分文字列に任意のオブジェクトを付加できることです。TextViewはSpannableインターフェースを認識し、レンダリング時に対応するテキストセグメントにスパンオブジェクトを適用します。通常のStringをTextViewに渡すと、スタイルは適用されません。
SpannableStringはテキストをchar[]配列として、スパンオブジェクトを開始位置と終了位置のメタデータとともに別の配列として格納します。setSpan(what, start, end, flags)が呼び出されると、whatオブジェクトは範囲情報とともにリストに保存されます。レンダリング時、TextViewは表示範囲内にあるすべてのスパンを順次適用し、updateDrawStateメソッドとupdateMeasureStateメソッドを呼び出します。
CharacterStyleは、行内の位置に関係なく個々の文字に影響を与えるスパンの基本クラスです。これにはForegroundColorSpan(テキストの色)、RelativeSizeSpan(相対サイズ)、StyleSpan(太字と斜体)、UnderlineSpan(下線)などが含まれます。文字スパンは指定された範囲内の各文字に個別に適用されます。
ParagraphStyleは段落全体に影響を与えるスパンのインターフェースです。最もよく知られている代表例はAlignmentSpanで、段落全体を左揃え、中央揃え、または右揃えにします。段落スパンは段落全体をカバーする必要があります。そうしないと、Androidはその適用を無視します。この制限は、配置やインデントがテキストブロック全体に対してのみ意味を持つためです。
Spanフラグは、範囲の境界でテキストが挿入または削除されたときのスパンの動作を定義する4つの定数です。SPAN_EXCLUSIVE_EXCLUSIVEは元の境界内でのみスパンをアクティブに保ち、SPAN_INCLUSIVE_INCLUSIVEは境界にテキストが追加されると拡張します。SPAN_EXCLUSIVE_INCLUSIVEとSPAN_INCLUSIVE_EXCLUSIVEは範囲の開始と終了で混合動作を提供します。
| フラグ | 左側への挿入 | 右側への挿入 |
|---|---|---|
| SPAN_EXCLUSIVE_EXCLUSIVE | 含まない | 含まない |
| SPAN_INCLUSIVE_INCLUSIVE | 含む | 含む |
| SPAN_EXCLUSIVE_INCLUSIVE | 含まない | 含む |
| SPAN_INCLUSIVE_EXCLUSIVE | 含む | 含まない |
正しいフラグの選択は、ユーザーが文字を挿入・削除できるEditTextのEditableテキストにとって重要です。読み取り専用のTextViewでは通常SPAN_EXCLUSIVE_EXCLUSIVEが使用され、スタイルは元の範囲にのみ適用され、プログラムによる変更では拡張されません。
Android SDKは25以上の組み込みスパンクラスを提供しており、ほとんどのテキストスタイル設定タスクをカバーします。各クラスはCharacterStyleまたはParagraphStyleインターフェースを実装し、コンストラクターを介してパラメーターを受け取ります。すべてのクラスはandroid.text.styleパッケージにあり、追加の依存関係なしで利用できます。
ForegroundColorSpanは指定された範囲のテキストの色を設定し、int形式で色を受け取ります。BackgroundColorSpanはテキストの背景を塗りつぶし、検索結果のハイライトに便利です。AbsoluteSizeSpanはピクセル単位で正確なフォントサイズを設定し、RelativeSizeSpanはTextViewのベーステキストサイズに対する乗数を適用します。
StyleSpanはTypeface.NORMAL、Typeface.BOLD、Typeface.ITALIC、またはBOLD_ITALICの定数を受け取り、文字の書体を変更します。UnderlineSpanは下線を追加し、StrikethroughSpanは取り消し線を追加します。SuperscriptSpanとSubscriptSpanは上付き文字と下付き文字を作成します。TypefaceSpanはテキスト範囲にTypefaceオブジェクトを介してカスタムフォントを設定できます。
ClickableSpanはクリック可能なテキストセグメントを作成するための抽象クラスです。タップするとonClick()メソッドが呼び出されます。クリックを機能させるには、TextViewにsetMovementMethod(LinkMovementMethod.getInstance())が必要です。URLSpanはハイパーリンク用のClickableSpanのサブクラスで、ブラウザを自動的に開きます。ClickableSpanはリンクを青く視覚的に強調するためにForegroundColorSpanと組み合わせられることがよくあります。
val spannable = SpannableString("Open developer.android.com")
spannable.setSpan(
URLSpan("https://developer.android.com"),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannable.setSpan(
ForegroundColorSpan(Color.BLUE),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()
LinkMovementMethodがないと、URLSpanのクリックは処理されません。MovementMethodはタッチイベントをインターセプトし、タッチ位置でClickableSpanを見つける役割を果たします。色スパンはリンクをユーザーに視覚的に認識させます。
SpannableStringの使用は、テキスト文字列からインスタンスを作成し、setSpan()メソッドを介してスパンを順次適用することから始まります。このメソッドは4つのパラメーター(スパンオブジェクト、開始位置、終了位置、フラグ)を受け取ります。すべてのスパンを設定した後、オブジェクトはsetText()を介してTextViewに渡されます。
最初の単語が赤く拡大された文字列を作成しましょう。これには、色にForegroundColorSpan、スケールにRelativeSizeSpanを使用します。両方のスパンは独立して同じ範囲に適用されます。setSpanの呼び出し順序は重要ではありません。
val text = "Header: remaining text"
val spannable = SpannableString(text)
val colorSpan = ForegroundColorSpan(Color.RED)
val sizeSpan = RelativeSizeSpan(1.5f)
spannable.setSpan(colorSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(sizeSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannable
位置0から9までの文字は両方のスタイルを同時に受け取ります。TextViewはレンダリング時にすべてのスパンを自動的に適用します。追加の呼び出しは必要ありません。1.5fの乗数を持つRelativeSizeSpanは、ベースに対してフォントサイズを50%拡大します。
Html.fromHtml()はHTML文字列からSpannedオブジェクトを作成しますが、サポートされるタグのセットは限られています。SpannableStringはHTMLの制限なしにすべての属性を完全に制御できます。HTMLをスパンに変換してからカスタムスタイルを追加する必要がある場合は、Html.fromHtml()をベースとして使用し、setSpan()を介してスパンを補完できます。
val htmlText = Html.fromHtml(
"<b>Important:</b> check the data",
Html.FROM_HTML_MODE_LEGACY
)
val spannable = SpannableString(htmlText)
spannable.setSpan(
ForegroundColorSpan(Color.RED),
0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
結果 — テキスト“重要:”は太字(HTMLから)で赤色(スパンから)になります。このアプローチは、書式設定の一部がHTMLで指定され、一部がクライアント側でプログラム的に追加されるサーバーコンテンツを扱う際に便利です。
SpannableStringは同じ範囲または重複する範囲に無制限の数のスパンを適用できます。スパンの組み合わせは、ネストされたタグが競合する可能性があるHTMLマークアップに対する主要な利点です。スパンは独立しており、レンダリング時に順次適用されます。
たとえば、テキストセグメントを同時に太字で赤くクリック可能にできます。これを行うには、StyleSpan、ForegroundColorSpan、ClickableSpanの3つのスパンを作成し、それぞれを同じ範囲に適用します。各スパンが独自のテキスト属性を担当するため、適用順序は結果に影響しません。
異なるタイプのスパンが部分的に重なる場合、各スパンは独自の境界内で独立して機能します。範囲0~10のForegroundColorSpanと範囲5~15のStyleSpan(BOLD)は、セグメント5~10では太字の赤いテキスト、10~15では太字のみになります。各スパンはレンダリング時に独自の属性を変更するため、競合は発生しません。
getSpans(int start, int end, Class type)メソッドは、指定された範囲内のスパンの配列を返します。これは、すでに適用されているスタイルを確認したり、特定のスパンを削除したりするのに便利です。nextSpanTransition()を使用すると、スパン変更の境界を反復処理できます。これは、スタイルがどこで変わるかを知る必要があるカスタムTextView実装の基礎です。
SpannableStringBuilderは、フラグメントの挿入、置換、削除が可能な、スタイル付きテキストを段階的に構築するためのクラスです。既製の文字列から作成され不変であるSpannableStringとは異なり、Builderはテキスト部分を順次追加し、その場でスタイルを割り当てることができます。ログ、チャット、動的ラベル付きのニュース見出しなどの複合メッセージに最適です。
BuilderはSpannableおよびEditableインターフェースを実装しており、EditTextと互換性があります。ユーザーはテキストを編集でき、スタイルは保存され、新しい文字が挿入されると正しくシフトされます。SpannableStringは不変であるため、編集可能なフィールドには適していません。
append()メソッドはビルダー自身を返すため、メソッドチェーンが可能です。テキスト追加後、setSpan()を介してスパンが適用されます。位置は現在のビルダー長を基準に指定されます。より正確なコンテンツ制御のためにInsert()やreplace()も利用できます。
val builder = SpannableStringBuilder()
.append("New ")
.append("comment")
val blue = ForegroundColorSpan(Color.BLUE)
val gray = ForegroundColorSpan(Color.GRAY)
builder.setSpan(blue, 0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
builder.setSpan(gray, 6, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = builder
テキスト“新規 ”は青色、“コメント”は灰色で色付けされています。それらの間に追加の文字を挿入しても、EXCLUSIVEフラグのおかげでスパンは新しいテキストに影響しません。Builderは変更時に内部インデックスを自動的に調整します。
多数のスパンを使用するとTextViewのレンダリングパフォーマンスに影響します。各スパンは再描画のたびにupdateDrawState()またはupdateMeasureState()メソッドを呼び出します。ミッドレンジデバイスで快適に動作させるには、TextViewあたりのスパン数を50~100に制限することをお勧めします。テキストサイズを変更するスパン(RelativeSizeSpan、AbsoluteSizeSpan)は、変更のたびにレイアウトの再計算が必要であり、色や下線のスパンよりも大幅にコストがかかります。
TextAppearanceSpanを使用すると、1回のsetSpan()呼び出しでandroid:textAppearance XMLリソースからスタイルの完全なセットを適用できます。3つの個別のスパン(色、サイズ、フォント)の代わりに、スタイル参照を持つ1つのTextAppearanceSpanが使用されます。これによりオブジェクト数が減り、メンテナンスが簡素化されます。リソースのスタイルを変更すると、このスパンを使用するすべてのテキストに自動的に適用されます。
setSpan()のたびに新しいスパンインスタンスを作成すると、ガベージコレクターに余分な負荷がかかります。繰り返し使用する場合は定数スパンオブジェクトを作成するのが最適です。たとえば、ForegroundColorSpan(Color.RED)はcompanionオブジェクトに保存して再利用できます。ただし、状態を持つスパン(異なるハンドラーを持つClickableSpan)は、ケースごとに個別に作成する必要があります。
スパンのパフォーマンス問題を診断するには、Android StudioのLayout InspectorとGPUプロファイラーを使用します。多くのスパンを持つTextViewがスクロール時に著しく遅くなる場合は、TextAppearanceSpanを介したいくつかのスパンを静的スタイルに置き換えるか、属性をカスタムUpdateAppearance実装に統合してスパン数を減らすことを検討してください。
よくある質問
Stringはスタイルをサポートしない不変の文字シーケンスです。SpannableStringは同じ文字を格納しますが、加えて書式情報を含むSpanオブジェクトの配列を持ちます。TextViewは渡されたCharSequenceのタイプを判別し、レンダリング時に対応する範囲にスパンを適用します。Stringはスタイル属性を無視し、プレーンテキストとして表示されます。
removeSpan(Object span)メソッドは特定のスパンを削除します。完全にクリーンアップするには、getSpans(0, length, Object::class.java)を呼び出してすべてのスパンの配列を取得し、それぞれをremoveSpanで削除します。または、スパンのない新しいSpannableString(text.toString())を作成します。SpannableStringBuilderにはテキストとスパンの両方を削除するclear()メソッドがあります。
SpannableStringはEditTextでも動作しますが、編集可能なテキストにはEditableを実装するSpannableStringBuilderが推奨されます。EditTextは変更を追跡するためにEditableインターフェースを必要とします。SpannableStringをEditTextに渡すとスタイル付きで表示されますが、編集中にAndroidがEditableに変換する際に一部のスパンがリセットされる可能性があります。
Jetpack ComposeではAndroid SDKのスパンは直接使用されません。代わりにComposeは、同様の機能を持つSpannableStringの独自の相当品であるAnnotatedStringを提供します。個々の文字スタイルにはSpanStyle、段落にはParagraphStyleを使用します。SpannableStringからAnnotatedStringへの変換は、スパンを反復処理するbuildAnnotatedStringを介して可能です。
CharacterStyleを拡張し、updateDrawState(TextPaint tp)メソッドをオーバーライドするクラスを作成します。メソッド内でTextPaintのプロパティ(色、ストローク幅、効果)を変更します。メトリック変更にはUpdateLayoutまたはMetricAffectingSpanを使用します。カスタムスパンは組み込みスパンと同様にsetSpan()を介して適用されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。