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


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

実装コード

早速サンプルコードから

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

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

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

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

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

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

アイコン以外を表示する

@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 以外でも表示させることができて、複数のコンテンツをテキストの中に表示させることができます。

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

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

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

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

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

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 を使う場合のテキストの読み上げは、 appendInlineContentalternateText で自然なテキストになるようにするか、Text 自体の semantics で設定してあげるのが良さそうです。

参考