Jetpack Composeで特定の部分だけ文字色を変える方法

:::note info
【備考】
HTMLCompatというクラスを使用してHTML文字列の部分だけ文字色を変える方法を学んだため記録として残します。
漏れや不備等があるかもしれないため、その点ご理解いただけますと幸いです:bow:
:::

## やりたいこと
▼こんな感じに一部だけ文字色を変えたい
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3172794/6cd2aaff-6d8d-4328-a77c-39c6b68beaff.png">


## ざっくり手順
① HTML文字列の準備する
② 再利用できるようにHTML形式の文字列を表示するためのComposable関数を作成する
③ 作成したComposable関数を必要な場所で呼び出して表示する


## ① HTML文字列の準備
HTML形式（例えば <font> タグで部分的に文字色を変えるなど）を使った文字列を用意します。

▼例
```
"ご覧いただきありがとうございます。HTML形式のテキストを<font color='#ed293e'>一部赤色の文字で表示します。</font>"
```

## ② Composable関数を作成
HTML形式の文字列を表示するためにComposable関数を作成します。
補足として、再利用しやすい命名にすると良さそうです。
以下細かな手順です。

### 1. HTMLCompatを使ってHTMLを変換する
`HtmlCompat.fromHtml`を使用して、HTML文字列を変換し、`spanned`という変数に格納します。

```kotlin
val spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
```

:::note info
【補足】
<dt>HtmlCompat.fromHtmlとは？</dt>
<dd>HtmlCompatはAndroidxに含まれる標準ライブラリ。
fromHtmlメソッドはHTML文字列をSpannedに変換してくれます。
Spannedは文字列にスタイルやフォーマットを適応するためのインターフェースなので、Spannedを使って特定の部分に色をつけていきます。
 </dt>
 
 参考：[公式ドキュメント](https://developer.android.com/reference/androidx/core/text/HtmlCompat
 "HtmlCompat")
:::


### 2. スタイルの適用
以下、要点となります。

1. `buildAnnotatedString`を使用して、「1. HTMLCompatを使ってHTMLを変換する」の`Spanned`にスタイルを適応します。【補足1】
1. `spanned.getSpans`メソッドを使用して、指定された範囲内のスタイルなどを取得します。【補足2】
1. `when`を使用して`ForegroundColorSpan`の場合、テキストの色を設定するよう条件分岐をします。【補足3】
1. `spanned.getSpanStart(it)`と`spanned.getSpanEnd(it)`を使用して、スタイルを適用する範囲を指定します。
1. 最後に`append(spanned)`で変換されたスタイルを追加します。

:::note info
<details><summary>【補足1】</summary>
  <dt>buildAnnotatedStringとは？</dt>
  <dd>テキストに複数の装飾付きの文字列を作成する関数です。</dd>
  </details>
:::
:::note info
<details><summary>【補足2】</summary>

<dt>CharacterStyleについて</dt>

[CharacterStyle](https://developer.android.com/reference/android/text/style/CharacterStyle "CharacterStyle")は文字のスタイルを適応するための抽象クラスで、具体的には以下のようなスタイルを適応するサブクラスがあります。

- ForegroundColorSpan: 文字の前景色（テキストの色）を変更します。
- BackgroundColorSpan: 文字の背景色を変更します。
- StyleSpan: 文字のスタイル（太字、斜体など）を変更します。
- UnderlineSpan: 文字に下線を引きます。
- StrikethroughSpan: 文字に取り消し線を引きます。

ここでは、Spannedの文字全体からスタイル適応に該当する文字部分を取得しているイメージです。

今回は文字色を変えたいのでForegroundColorSpanが返ってくることを期待します！
  
参考：[公式ドキュメント](https://developer.android.com/reference/android/text/Spanned#getSpans(int,%20int,%20java.lang.Class%3CT%3E) "getSpans")
   </details>
:::


:::note info
<details><summary>【補足3】</summary>
<dt>whenを使っている理由は？</dt>
<dd>CharacterStyleのインスタンスがForegroundColorSpanである場合に、色情報を取得してSpanStyleに適用するため。その他の型の場合は何もしません。</dd>
</details>
:::

<details open><summary>サンプルコード（閉じれます）</summary>


```kotlin
val annotatedText = buildAnnotatedString {
        spanned.getSpans(0, spanned.length, CharacterStyle::class.java).forEach {
            // フォントのスパンにスタイルを適用
            when (it) {
                is ForegroundColorSpan -> {
                    // itはForegroundColorSpanのインスタンス
                    val color = it.foregroundColor
                    addStyle(
                        SpanStyle(
                            color = Color(color),
                        ),
                        spanned.getSpanStart(it),
                        spanned.getSpanEnd(it),
                    )
                }
                else -> {
                    // 何もしない
                }
            }
        }
        append(spanned)
    }
```
</details>


### 3. テキストの表示
スタイルが適用されたテキストを表示します。
```kotlin
Text(
    modifier = modifier,
    text = annotatedText,
    style = textStyle.merge(
         TextStyle(fontSize = fontSize),
    ),
)
```


### ここまでの全コード
<details open><summary>ここまでのコード（閉じれます）</summary>

```kotlin
@Composable
fun HtmlText(
    html: String,
    fontSize: TextUnit,
    modifier: Modifier = Modifier,
    textStyle: TextStyle = LocalTextStyle.current,
) {
    val spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
    val annotatedText = buildAnnotatedString {
        spanned.getSpans(0, spanned.length, CharacterStyle::class.java).forEach {
            when (it) {
                is ForegroundColorSpan -> {
                    val color = it.foregroundColor
                    addStyle(
                        SpanStyle(
                            color = Color(color),
                        ),
                        spanned.getSpanStart(it),
                        spanned.getSpanEnd(it),
                    )
                }
                else -> {
                    // 何もしない
                }
            }
        }
        append(spanned)
    }
    Text(
        modifier = modifier,
        text = annotatedText,
        style = textStyle.merge(
            TextStyle(fontSize = fontSize),
        ),
    )
}
```
</details>


## ③ 作成したComposable関数を必要な場所で呼び出して表示する
```kotlin
@Composable
fun SampleScreen() {
    val sampleHtml = "ご覧いただきありがとうございます。HTML形式のテキストを<font color='#ed293e'>一部赤色の文字で表示します。</font>"

    HtmlText(
        html = sampleHtml,
    )
}
```

▼Previewですがこんな感じになります
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3172794/6cd2aaff-6d8d-4328-a77c-39c6b68beaff.png">


以上となります。
最後までご覧いただきありがとうございました:thumbsup_tone1: