こんにちは、Android開発してるめっしーです。
Twitterはこちらです
始めに
Android(kotlin)で文字列を装飾する際に利用するスパンについて紹介します。
下の方にktx使った簡単なものも書きました。
スパンとは
スパンは強力なマークアップオブジェクトで、文字単位や段落単位でテキストのスタイルを設定できます。
スパンをテキストオブジェクトにアタッチすると、色、テキストのクリック可視化、文字サイズの拡大縮小、など様々な方法でテキストを変更できます。
種類
スパンを作成するには、以下のリスト表示されているクラスを用途に応じて利用する。
クラス | テキストは変更可 | マークアップは変更可 | データ構造 |
---|---|---|---|
SpannedString | × | × | リニア配列 |
SpannableString | × | ○ | リニア配列 |
SpannableStringBuilder | ○ | ○ | 区間ツリー |
SpannedString
テキストやマークアップを作成後に変更しない場合に利用
SpannableString
単一のテキストオブジェクトに少数のスパンをアタッチした、テキスト自体は読み取り専用にする場合に利用。
SpannableStringBuilder
作成後にテキストを変更する必要があり、テキストにスパンをアタッチする必要がある場合
テキストオブジェクトに多数のスパンをアタッチする必要がある場合は、テキスト自体を読み取り専用にするかどうかにかかわらず利用
実装
基本形
スパン適用するには
setSpan(Object what, int start, int end, int flags)
を呼び出す
val spannable = SpannableStringBuilder("Sample Text")
spannable.setSpan(
ForegroundColorSpan(Color.RED),
6, // start
10, // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
flag | 説明 |
---|---|
Spannable.SPAN_EXCLUSIVE_INCLUSIVE | 挿入テキストを含める場合 |
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | 挿入テキストを除外する場合 |
文字のRangeをとる拡張関数
Spanを使う時に文字の一部を装飾したい機会が多いと思われます。
その際に特定の文字のRangeを取得できるものがあると便利なので以下のような拡張関数を作ります。
この後のサンプルもこれを使ったもので説明していきます。
fun String.rangeOfIndex(string: String): IntRange {
val startIndex = indexOf(string)
return startIndex until startIndex + string.length
}
KTX
ktxの拡張機能にもスパンがあります
依存関係
dependencies {
implementation("androidx.core:core-ktx:1.5.0")
}
スパンでできるスタイル
スパンで可能となってる装飾を色々紹介していきます。
ktxで出来るものはその実装も一緒に載せておきます。
色
ForegroundColorSpan(Color.RED)
を利用
private fun spanColor(): SpannableStringBuilder {
val sampleLabel = "Sample Text"
val colorRange = sampleLabel.rangeOfIndex("Text")
return SpannableStringBuilder(sampleLabel).apply {
setSpan(
ForegroundColorSpan(Color.RED),
colorRange.first, // start
colorRange.last.inc(), // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
//kts
private fun spanColorKtx(): SpannedString {
val sampleLabel = "Sample Text"
val colorRange = sampleLabel.rangeOfIndex("Text")
return buildSpannedString {
append(sampleLabel.subSequence(0, colorRange.first))
color(Color.RED) {
append(sampleLabel.subSequence(colorRange))
}
}
}
アンダーライン
private fun spanUnderline(): SpannableStringBuilder {
val sampleLabel = "Sample Text"
val underlineRange = sampleLabel.rangeOfIndex("Text")
return SpannableStringBuilder(sampleLabel).apply {
setSpan(
UnderlineSpan(),
underlineRange.first, // start
underlineRange.last.inc(), // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
//ktx
private fun spanUnderlineKtx(): SpannedString {
val sampleLabel = "Sample Text"
val underlineRange = sampleLabel.rangeOfIndex("Text")
return buildSpannedString {
append(sampleLabel.subSequence(0, underlineRange.first))
underline {
append(sampleLabel.subSequence(underlineRange))
}
}
}
文字サイズ
50%大きくする場合に
private fun spanSize(): SpannableStringBuilder {
return SpannableStringBuilder("Sample Text").apply {
setSpan(
RelativeSizeSpan(1.5f),
7, // start
this.length, // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
//kts
private fun spanSizeKtx(): SpannedString {
val sampleLabel = "Sample Text"
val underlineRange = sampleLabel.rangeOfIndex("Text")
return buildSpannedString {
append(sampleLabel.subSequence(0, underlineRange.first))
scale(1.5f) {
append(sampleLabel.subSequence(underlineRange))
}
}
}
背景に色を
BackgroundColorSpan()
を利用
private fun spanBackgroundColor(): SpannableStringBuilder {
return SpannableStringBuilder("Sample Text").apply {
setSpan(
BackgroundColorSpan(Color.RED),
7, // start
this.length, // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
//ktx
private fun spanBackgroundKtx(): SpannedString {
val sampleLabel = "Sample Text"
val underlineRange = sampleLabel.rangeOfIndex("Text")
return buildSpannedString {
append(sampleLabel.subSequence(0, underlineRange.first))
backgroundColor(Color.RED) {
append(sampleLabel.subSequence(underlineRange))
}
}
}
段落
private fun spanParagraph(): SpannableStringBuilder {
return SpannableStringBuilder("Sample \nText").apply {
setSpan(
QuoteSpan(),
0, // start
this.length, // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
API28からは段落の横のスタイルを変えることもできます。
@RequiresApi(Build.VERSION_CODES.P)
private fun spanParagraph(): SpannableStringBuilder {
return SpannableStringBuilder("Sample \nText").apply {
setSpan(
QuoteSpan(Color.GREEN, 20, 40),
0, // start
this.length, // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
クリック処理
private fun spanClick(): SpannableStringBuilder {
val sampleLabel = "Sample Text"
val sizeRange = sampleLabel.rangeOfIndex("Text")
return SpannableStringBuilder(sampleLabel).apply {
setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) {
// TODO クリック処理
}
},
sizeRange.first, // start
sizeRange.last.inc(), // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
//ktx
private fun spanClickKtx(): SpannedString {
val sampleLabel = "Sample Text"
val sizeRange = sampleLabel.rangeOfIndex("Text")
return buildSpannedString {
append(sampleLabel.subSequence(0, sizeRange.first))
inSpans(
object : ClickableSpan() {
override fun onClick(widget: View) {
// TODO クリック処理
}
},
) {
append(sampleLabel.subSequence(sizeRange))
}
}
}
クリック処理を行うには以下も忘れずに
termsText.apply {
text = spanClick()
//追加
movementMethod = LinkMovementMethod.getInstance()
}
movementMethod
TextViewにナビゲーションの目的で、キーイベント、トラックボール、モーション、およびタッチの処理をmoveメソッドに委任します。
LinkMovementMethod
テキスト内のリンクを移動し、必要に応じてスクロールするためのクラス。
複数のスパン
1つのテキストに複数のスパンをアタッチできる
private fun spanMultiple(): SpannableString {
val sampleLabel = "Sample Text"
val boldRange = sampleLabel.rangeOfIndex("Text")
val colorRange = sampleLabel.rangeOfIndex("Te")
return SpannableString(sampleLabel).apply {
setSpan(
ForegroundColorSpan(Color.RED),
colorRange.first, // start
colorRange.last.inc(), // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
setSpan(
StyleSpan(Typeface.BOLD),
boldRange.first,
boldRange.last.inc(),
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
}
}
サンプルレポジトリ
こちらは自分が行ってきた技術サンプルが詰まってます。
その中で今回は
SpanFragment.kt
に今回のコードが書かれてあります。ぜひご覧ください。
参考
ではまた!