JetpackComposeでSpannableテキスト

# Spannable風のTextを作る

![text_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/ba2ba372-a14f-82ff-0983-48456b62c07a.png)

この画像の年月日の部分は、1つのTextに対して、複数のスタイルを適用する（Spannable風）ことで実現しています。
以下を参考にしています。

:::note info
[1つのテキストに複数のスタイルを設定する(Google Developers)](https://developer.android.com/jetpack/compose/text?hl=ja#multiple-styles)
:::

## 一部のフォントサイズを小さくする
```kotlin
    val smallSpanStyle = SpanStyle(fontSize = MaterialTheme.typography.bodySmall.fontSize)
    Text(
        text = buildAnnotatedString {
            append(day.format("yy"))
            withStyle(style = smallSpanStyle) {
                append("年")
            }
            append(day.format("M"))
            withStyle(style = smallSpanStyle) {
                append("月")
            }
            append(day.format("d"))
            withStyle(style = smallSpanStyle) {
                append("日")
            }
        },
        style = MaterialTheme.typography.titleLarge,
        modifier = modifier,
    )
```
Spannable風のテキストを作るには、buildAnnotatedStringを使用します。
基本のスタイルは、Textのstyleに「MaterialTheme.typography.titleLarge」を指定し、
年月日の部分は、SpanStyle（fontSize = 小さめ）を指定しています。

## 応用
私の場合、Spannableを業務利用するのでよく使うのが、
「<INS>利用規約</INS>と<INS>プライバシーポリシー</INS>に同意...」ですかね。
後は、電話番号で電話かけたりとかもしますが、ともかく

- アンダーラインを引いて
- クリックイベントを拾って
- なんかする

が一番多いですので、それを作ってみます。

### 自作LinkTextコンポーネント
先ほどとは違い、Styleと文字を順に追加していくのではなく、
文字全部を表示して、後からこの文字を装飾したい！みたいな使い方を想定しています。
[GitHubのソースはこちら](https://github.com/jozuko/ComposeTutorial/blob/242c4abfd8e5dfce407b03b9205f6d1f87cce974/app/src/main/java/com/jozu/jetpack/compose/tutorial/screen/composable/LinkText.kt)

```kotlin:LinkText.kt
@OptIn(ExperimentalTextApi::class)
@Composable
fun LinkText(
    text: String,
    linkTextList: List<LinkTextData>,
    modifier: Modifier = Modifier,
    onClick: (url: String) -> Unit,
) {
    // ①文字装飾定義
    val linkSpanStyle = SpanStyle(
        color = Color.Blue,
        textDecoration = TextDecoration.Underline,
    )

    // ②装飾文字生成
    val annotatedText = buildAnnotatedString {
        append(text)
        linkTextList.forEach { linkData ->
            val start = text.indexOf(linkData.label)
            if (start >= 0) {
                val end = start + linkData.label.length
                // ③UrlAnnotationを指定
                addUrlAnnotation(UrlAnnotation(linkData.url), start, end)
                // ④同じ場所に①の文字装飾を指定
                addStyle(linkSpanStyle, start, end)
            }
        }
    }

    ClickableText(
        text = annotatedText,
        modifier = modifier,
        onClick = { pos ->
            // ⑤タップ位置からUrlAnnotaionを取得
            annotatedText.getUrlAnnotations(start = pos, end = pos).firstOrNull()?.let { range ->
                onClick.invoke(range.item.url)
            }
        },
    )
}

data class LinkTextData(val label: String, val url: String)

```
annotationとして、文字装飾用とクリックイベント用の2種類のannotationが必要です。
リンクにしたい文字の位置をstart, endで取得して、①文字装飾と③UrlAnnotationを指定します。
クリックが発火したときに、発火位置から③で生成したUrlAnnotationを取得して、とれればイベントを発火させています。

### 使う側
こんな感じで使います。
リンクを押すと、uriHandlerを経由して外部ブラウザで各URLが開きます。
```kotlin
val uriHandler = LocalUriHandler.current
LinkText(
    text = "GoogleもしくはYahooを開きます。",
    linkTextList = listOf(
        LinkTextData("Google", "https://www.google.co.jp/"),
        LinkTextData("Yahoo", "https://www.yahoo.co.jp"),
    ),
    modifier = Modifier.padding(8.dp),
    onClick = { url ->
        uriHandler.openUri(url)
    },
)
```
![linktext_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/9576a843-a8fd-286c-9806-03636636cb0c.png)