Composeで縁取り文字を描画しよう

# 縁取り文字とは

文字の色とは違う色などで縁取られた文字列のこと。

https://x.com/m4kvn/status/1740465324414411201?s=20

# 実装

縁取り文字を実現するためには、なるべくならComposeで提供されているTextをそのまま使えると嬉しい。ということで、ここではComposeのTextをベースとして実装する。そのためまずはTextを拡張するためのComposableとしてOutlinedTextを定義する。TextをラップするようなComposableを定義する際には、TextStyleを受け取るようにしておこう。

```kotlin
@Composable
fun OutlinedText(
    text: String,
    modifier: Modifier = Modifier,
    textStyle: TextStyle = TextStyle.Default,
) {
    Text(
        text = text,
        style = textStyle,
        modifier = modifier
    )
}
```

この時点では、普通に文字列が描画されるだけなのでここから縁取り部分（Stroke）を描画するようにしていく。Text自体にそれを実現できるようなパラメータはない。なので `Modifier.drawBehind()` を使って通常の文字の描画の後ろに縁取り部分を描画していくことにする。

```kotlin
@Composable
fun OutlinedText(...) {
    Text(
        ...,
        modifier = modifier
            .drawBehind {
                drawText(
                    textLayoutResult = TODO(),
                    drawStyle = Stroke(),
                    color = Color.Magenta,
                )
            }
    )
}
```

このときdrawTextでTextと同様のTextLayoutResultを渡す必要がある。そのため、Textのレイアウトが計算された際にそれを保持するようにし利用する。

```kotlin
@Composable
fun OutlinedText(...) {
    var textLayoutResult: TextLayoutResult? by remember {
        mutableStateOf(null)
    }
    Text(
        ...,
        onTextLayout = { textLayoutResult = it },
        modifier = modifier
            .drawBehind {
                textLayoutResult?.let {
                    drawText(
                        textLayoutResult = it,
                        drawStyle = Stroke(),
                        color = Color.Magenta,
                    )
                }
            }
    )
}
```

あとはdrawTextに渡すStrokeに `width` を設定する。このとき必要になる値が画面上のPixelにあたるFloatなので、DpからPxに変換してから使うようにする。

```kotlin
@Composable
fun OutlinedText(...) {
    ...
    val strokeWidth = with(LocalDensity.current) { 4.dp.toPx() }
    Text(
        ...,
        modifier = modifier
            .drawBehind {
                textLayoutResult?.let {
                    drawText(
                        textLayoutResult = it,
                        drawStyle = Stroke(width = strokeWidth),
                        color = Color.Magenta,
                    )
                }
            }
    )
}
```

この時点でPreviewしてあげると次のようになる。

![outlined_text_preview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80866/702d11c3-3659-80ac-f5eb-b6e27e4ef55c.png)

```kotlin
@Preview
@Composable
private fun OutlinedTextPreview() {
    val text = "OutlinedText"
    val textStyle = TextStyle(
        color = Color.White,
        fontSize = 40.sp,
        fontWeight = FontWeight.ExtraBold,
    )
    MyTheme {
        OutlinedTextQiita(
            text = text,
            textStyle = textStyle,
        )
    }
}
```

# 完成

あとはコードを整理して最終的には次のようなComposableが完成する。

```kotlin
@Composable
fun OutlinedText(
    text: String,
    modifier: Modifier = Modifier,
    textStyle: TextStyle = TextStyle.Default,
    stroke: Stroke = Stroke(),
    strokeColor: Color = Color.Transparent,
) {
    var textLayoutResult: TextLayoutResult? by remember {
        mutableStateOf(null)
    }
    Text(
        text = text,
        style = textStyle,
        onTextLayout = {
            textLayoutResult = it
        },
        modifier = modifier
            .drawBehind {
                textLayoutResult?.let {
                    drawText(
                        textLayoutResult = it,
                        drawStyle = stroke,
                        color = strokeColor,
                    )
                }
            }
    )
}
```

# まとめ

今回はComposeから提供されているTextを使って、縁取り文字を描画するComposableを作成することができた。この実装で重要な点は次のようなものがある。

- TextをラップするComposableではTextStyleを受け取ると楽
- Textのレイアウトの情報は `onTextLayout` で取得できる
- drawTextの `drawStyle` で描画のスタイルを指定できる

これらの点や仕組みを知ることで今回のような文字列を描画することができるようになった。実際にプロダクトで利用できるかどうかはまだわからないが仕組みとしては面白いと思ったし、やってみた楽しかった。