3
4

More than 1 year has passed since last update.

Jetpack Composeで真ん中省略のテキストを実装する

Last updated at Posted at 2023-08-05

Jetpack Compose の Text の API では overflow = TextOverflow.Ellipsis で末尾省略は可能ですが、真ん中省略の API はありません。
TextView では maxLines = 1 のとき限定で真ん中省略が可能でした。
Jetpack Compose を使いつつテキストの真ん中省略はどうしたら実現できそうかいくつか試してみたのでそのメモです。

実装

A. TextUtils#ellipsize を使う

TextUtils#ellipsize という Android の API を使用することで、領域に入る文字数の文字列を省略記号付きで返してくれるメソッドがあります。

これと Jetpack Compose の Text を組み合わせて表示させる方法です。

@Composable
fun MiddleTruncatedText(
    text: String,
    modifier: Modifier = Modifier,
    style: TextStyle = LocalTextStyle.current,
    color: Color = LocalContentColor.current,
) {
    val density = LocalDensity.current
    val noLetterSpacingStyle = style.copy(letterSpacing = 0.sp)

    BoxWithConstraints(modifier = modifier) {
        val paint = remember(density, style) {
            TextPaint().apply {
                textSize = with(density) { noLetterSpacingStyle.fontSize.toPx() }
                letterSpacing = 0f
            }
        }
        val truncatedText = TextUtils.ellipsize(text, paint, constraints.maxWidth.toFloat(), TruncateAt.MIDDLE)
        Text(
            text = truncatedText,
            maxLines = 1,
            style = noLetterSpacingStyle,
        )
    }
}

Good

  • Jetpack Compose の Text を使っての実装が可能
  • 実装コストが低い

Bad

  • 実際に表示する時に使う TextStyle と計算に使用する TextPaint の調整が必要
    • TextPaint を使ってどのくらい文字が入るかを計算しているので、調整がズレると領域以上にテキストが長くなったりすることがあり得る
  • BoxWithConstraints を使っているので使い方次第ではパフォーマンスが落ちる

B. TextView を使う

AndroidView を使って TextView を埋め込み、真ん中省略を実現する方法です。
TextView であれば真ん中省略は用意されている API で実現可能です。

@Composable
fun MiddleTruncatedText(
    text: String,
    modifier: Modifier = Modifier,
    style: TextStyle = LocalTextStyle.current,
    color: Color = LocalContentColor.current,
) {
    AndroidView(
        factory = {
            TextView(it).also { textView ->
                textView.textSize = style.fontSize.value
                textView.maxLines = 1
                textView.ellipsize = TruncateAt.MIDDLE
                textView.setTextColor(color.toArgb())
            }
        },
        update = {
            it.text = text
        },
        modifier = modifier
    )
}

Good

  • TextView の API で真ん中省略は可能なので実装は簡単
  • 実装コストが低い

Bad

  • TextView の style と Jetpack Compose のテキストの装飾に使用する TextStyle の変換が必要で、Jetpack Compose の Text と同じような見た目にするための調整が必要
    • Jetpack Compose の Text と並べるレイアウトでなけれ無視はできる
  • 純粋な Jetpack Compose の Text を使うよりもパフォーマンスが落ちる
    • 詳細までは調べていないがおそらく TextView のインスタンス生成や measure 周り

C. 表示領域とフォントサイズから文字列の操作処理を自前で実装する

フォントサイズとレイアウトのサイズを使って領域可能な文字数を計算して、表示したい文字列を変えることで実現できるとは思いますが、一番大変そうなので試していません…

Good

  • 自前実装にすると真ん中省略だけでなくどんなパターンでも実装次第で実現可能
    • 本来はできない複数行での真ん中省略(2行目だけ真ん中省略させたい)とか

Bad

  • 実装コストが高い

【おまけ】パフォーマンス計測

A と B の実装ではどちらがパフォーマンスいいのか Macrobenchmark で計測してみます。

@Composable
fun TextList() = trace("TextList") {
    LazyColumn(
        modifier = Modifier.testTag("list")
    ) {
        items(100) {
            ListItem(headlineContent = {
                MiddleTruncatedText(longText)
            })
        }
    }
}

このような 100 件のリストをスクロールした時の計測をしてみます。

@OptIn(ExperimentalMetricApi::class)
private fun benchmark(compilationMode: CompilationMode) {
    rule.measureRepeated(
        packageName = "com.example.myapplication",
        metrics = listOf(
            FrameTimingMetric(),
            TraceSectionMetric("TextList")
        ),
        compilationMode = compilationMode,
        startupMode = StartupMode.WARM,
        iterations = 5,
        setupBlock = {
            pressHome()
        },
        measureBlock = {
            startActivityAndWait()
            device.wait(Until.hasObject(By.res("list")), 3_000)
            repeat(3) {
                textListScrollDownUp()
            }
        }
    )
}

private fun MacrobenchmarkScope.textListScrollDownUp() {
    val list = device.findObject(By.res("list"))
    device.flingElementDownUp(list)
}

private fun UiDevice.flingElementDownUp(element: UiObject2) {
    element.setGestureMargin(displayWidth / 5)

    element.fling(Direction.DOWN)
    waitForIdle()
    element.fling(Direction.UP)
}

このような処理で実行してみると

A B TextOverflow.Ellipsis の通常の Text
スクリーンショット 2023-08-05 17.14.40.png スクリーンショット 2023-08-05 17.19.37.png スクリーンショット 2023-08-05 17.28.28.png
frameDurationCpuMs
P50 11.0, P90 14.6, P95 26.8, P99 58.3
frameDurationCpuMs
P50 11.1, P90 18.6, P95 34.9, P99 68.2
frameDurationCpuMs
P50 11.1, P90 14.0, P95 15.7, P99 39.4

このような結果で、A の方がパフォーマンスがほんの少しいいということになります。
ちなみに、overflow = TextOverflow.Ellipsis の標準の API を使った場合も添えていますが、その場合はやはり標準の API だけの方が一番パフォーマンスがいいという結果になります。
とはいえ体感するほど顕著な差があるかと言われるとそうでもないので要件や実装しやすさで選んでも良さそうには思います。


そのほかに実現できる方法があれば教えてください :pray:

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4