Jetpack Composeでテキストの中にアイコンを埋め込んで表示させる

Jetpack Compose では簡単にテキストの中にアイコンを埋め込むことができたので紹介しようと思います。

## 実装コード

早速サンプルコードから

```kotlin
@Composable
fun Example() {
    val iconId = "iconId"
    Text(
        text = buildAnnotatedString {
            appendInlineContent(iconId)
            append("Hoge")
            appendInlineContent(iconId)
            append("Fuga")
        },
        inlineContent = mapOf(
            iconId to InlineTextContent(
                placeholder = Placeholder(
                    width = 24.sp,
                    height = 24.sp,
                    placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
                ),
                children = {
                    Icon(
                        painter = painterResource(id = R.drawable.ic_android),
                        contentDescription = null,
                    )
                },
            ),
        ),
    )
}
```

上記のコードを動かすとテキストの中にアイコンを表示させることができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/27bb08b6-f1f8-dc53-a658-a65316f048b4.png" width="200px" />

テキストが長い場合でもアイコンがテキストの中に埋め込まれる形になるので、改行された後のテキストとアイコンのラインを揃えるデザインにも対応できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/dc4a7068-7036-8059-cc78-554e67566356.png" width="200px" />


ざっくり実装を説明すると、

- 表示させたいアイコンのキーとなる文字列を定義  
- `InlineTextContent` にコンテンツを表示させる領域の指定と、表示させたいコンテンツの Composable を設定する
- `Text` の　`inlineContent` に定義したキーと `InlineTextContent` を `Pair` で関連づけて渡す
- `buildAnnotatedString` の中で表示したいタイミングで　`appendInlineContent`　に定義したキーとなる文字列を渡すと `Text` の中で `InlineTextContent` で設定した Composable が表示される

という感じになっています。


## アイコン以外を表示する

```kotlin
@Composable
fun Circle(
    modifier: Modifier = Modifier,
    color: Color = MaterialTheme.colors.primary,
) {
    Canvas(modifier = modifier) {
        drawCircle(
            color = color,
        )
    }
}

@Composable
fun Example() {
    val iconId = "iconId"
    val circleId = "circleId"
    Text(
        text = buildAnnotatedString {
            appendInlineContent(iconId)
            append("Hoge")
            appendInlineContent(circleId)
            append("Fuga")
        },
        inlineContent = mapOf(
            iconId to InlineTextContent(
                Placeholder(
                    width = 24.sp,
                    height = 24.sp,
                    placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
                ),
                children = {
                    Icon(
                        painter = painterResource(id = R.drawable.ic_android),
                        contentDescription = null,
                    )
                },
            ),
            circleId to InlineTextContent(
                placeholder = Placeholder(
                    width = 10.sp,
                    height = 10.sp,
                    placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
                ),
                children = {
                    Circle(
                        Modifier.size(8.dp)
                    )
                },
            )
        ),
    )
}
```

上記のコードを動かすと以下のようになり、`Icon` 以外でも表示させることができて、複数のコンテンツをテキストの中に表示させることができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/e07de610-dc5e-5436-89f1-af51cd8c8516.png" width="200px" />



## アイコンが含まれるテキストの読み上げについて

`appendInlineContent()` にはキーとなる文字列以外にも `alternateText` を設定できるようになっており、Talkback で読み上げる場合はこの `alternateText` が読み上げられます。


```kotlin
Text(
    text = buildAnnotatedString {
        appendInlineContent(iconId, "Android")
        append("Hoge")
        appendInlineContent(iconId)
        append("Fuga")
    },
)
```

このコードの場合は 
`Android Hoge Fuga`
と読み上げられます。

コンテンツ側の `Icon` に `contentDescription` を設定した場合は、 `contentDescription` がテキストの読み上げられた最後にまとめて読み上げられます。

```kotlin
Text(
    text = buildAnnotatedString {
        appendInlineContent(iconId)
        append("Hoge")
        appendInlineContent(iconId)
        append("Fuga")
    },
    inlineContent = mapOf(
        iconId to InlineTextContent(
            placeholder = Placeholder(
                width = 24.sp,
                height = 24.sp,
                placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
            ),
            children = {
                Icon(
                    painter = painterResource(id = R.drawable.ic_android),
                    contentDescription = "Android",
                )
            },
        ),
    ),
)
```

このコードの場合は 
`Hoge Fuga　Android画像 Android画像`
と読み上げられます。

そのため、`appendInlineContent` を使う場合のテキストの読み上げは、 `appendInlineContent` の `alternateText` で自然なテキストになるようにするか、`Text` 自体の `semantics` で設定してあげるのが良さそうです。


## 参考

https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/InlineTextContent