9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Kotlin】SpanでTextの一部装飾

Last updated at Posted at 2021-06-08

こんにちは、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)を利用

スクリーンショット 2021-06-08 2.20.50.png

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))
    }
  }
}

アンダーライン

UnderlineSpan()を利用
スクリーンショット 2021-06-08 2.23.55.png

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))
    }
  }
}

文字サイズ

RelativeSizeSpan(1.5f)を利用
スクリーンショット 2021-06-08 2.24.24.png

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()を利用

スクリーンショット 2021-06-08 2.24.48.png

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))
    }
  }
}

段落

QuoteSpan()を利用
スクリーンショット 2021-06-08 13.55.22.png

private fun spanParagraph(): SpannableStringBuilder {
    return SpannableStringBuilder("Sample \nText").apply {
      setSpan(
        QuoteSpan(),
        0, // start
        this.length, // end
        Spannable.SPAN_EXCLUSIVE_INCLUSIVE
      )
    }
  }

API28からは段落の横のスタイルを変えることもできます。

スクリーンショット 2021-06-08 14.02.12.png

@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
      )
    }
  }

クリック処理

スクリーンショット 2021-06-08 2.25.47.png

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つのテキストに複数のスパンをアタッチできる

例)色とsyleを指定する
スクリーンショット 2021-06-08 2.22.29.png

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に今回のコードが書かれてあります。ぜひご覧ください。

参考

ではまた!

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?