TextMeasurerはJetpack ComposeのAPIで、画面上に実際にレンダリングする前にテキストの寸法を測定するために設計されています。Android ViewのPaint.measureTextによる従来のアプローチとは異なり、TextMeasurerはスタイルと制約を考慮しながら、幅、高さ、行のメトリクスを取得するComposeネイティブな方法を提供します。Android Developers Documentation(2025)によると、TextMeasurerはカスタムLayoutコンポーネント、アニメーション化されたラベル、テキストサイズが他の要素の配置に影響を与えるアダプティブUI要素で積極的に使用されています。
重要なポイント
TextMeasurerは、androidx.compose.ui.textパッケージの機能インターフェースで、実際に画面にレンダリングすることなくComposeでテキストを測定できます。これはComposeテキストエンジンの一部であり、Skiaを使用してレンダリングするParagraph APIレベルで動作します。
TextMeasurerの主な目的は事前測定です。Composeでは、テキストサイズは通常、ComposableがLayoutに配置された後に決定されます。しかし、レイアウトの前にテキストの幅を知る必要があるシナリオがあります。たとえば、文字列がコンテナに収まるかどうかを判断したり、テキストの長さに基づいてボタンのサイズを計算したりする場合です。
TextMeasurerはmeasure()メソッドを提供することでこれを解決します。このメソッドはTextMeasureRequest(テキスト、スタイル、制約)を受け取り、TextLayoutResult(メトリクス、行、文字)を返します。これにより、開発者は1フレームの遅延なく、すべてのテキストデータを同期的に取得できます。
テキストサイズが親コンポーネントのサイズに影響する場合、アニメーションテキストトランジションを作成する場合、またはテキストが他の要素と共存するカスタムLayoutを実装する場合にTextMeasurerを使用してください。
TextMeasurer APIは2つの主要な要素で構成されています。rememberTextMeasurer()(インスタンスの作成)とmeasure()(測定の実行)です。インスタンスはコンポーネントのライフサイクル全体で1回作成され、すべての測定で再利用されます。
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Hello, Compose!",
style = MaterialTheme.typography.bodyLarge,
constraints = Constraints(maxWidth = 200, maxHeight = 50)
)
// result.size.width, result.size.height, result.lineCount
constraintsパラメータは、テキストが収まるべき最大幅と高さを定義します。テキストが幅に収まらない場合、次の行に折り返されます。測定結果にはTextLayoutResultが含まれ、行数、各文字の位置、各行の幅、およびテキスト全体のサイズが提供されます。
TextMeasurerはUIスレッドで同期的に動作し、再コンポジションをトリガーしません。ただし、異なるスタイルで頻繁にテキストを測定するとオーバーヘッドが発生する可能性があるため、インスタンスはrememberを介して作成され再利用されます。
最も単純なシナリオは、親コンテナのサイズを決定するために単一行テキストの幅を測定することです。たとえば、ラベルの長さに応じて幅が決まるボタンの場合です。
val textMeasurer = rememberTextMeasurer()
var label by remember { mutableStateOf("Submit") }
val textResult = remember(label) {
textMeasurer.measure(
text = label,
style = MaterialTheme.typography.labelLarge,
constraints = Constraints(maxWidth = 1000, maxHeight = 100)
)
}
Box(
modifier = Modifier
.width(textResult.size.width.dp)
.height(40.dp)
.background(Color.Blue)
.clickable { onClick() }
) {
Text(text = label, style = MaterialTheme.typography.labelLarge)
}
この例では、remember(label)により、テキストが変更された場合にのみ測定が再開されます。制約は、行の折り返しを防ぐために大きなmaxWidth(1000)で設定されています。実際の制約はテキストの幅と等しくなります。Boxの幅は測定されたテキスト幅に設定され、“テキストに合わせたボタン”効果が得られます。
注:結果の幅は.dpを使用してdpに変換する必要があります。TextMeasurerはピクセル単位で寸法を返しますが、Modifier.widthはDpを期待するためです。正確性を確保するには、densityとLocalDensity.currentを使用してください。
複数行テキストの場合、TextMeasurerは全高だけでなく、行数と段落内の各文字の位置も決定できます。これはカスタムテキストフィールド、チャット、またはエディタを作成する際に必要です。
複数行テキストの主なTextLayoutResultプロパティ:lineCount(行数)、getLineTop(index)(行開始のY座標)、getLineBottom(index)(行終了のY座標)、getLineWidth(index)(行幅)。これらを組み合わせることで、テキスト行に対してインターフェース要素を正確に配置できます。
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Long multiline text that needs to be measured before layout",
style = MaterialTheme.typography.bodyMedium,
constraints = Constraints(maxWidth = 150, maxHeight = 300)
)
val linesCount = result.lineCount
val firstLineWidth = result.getLineWidth(0)
val totalHeight = result.size.height
getLineWidth(lineIndex)メソッドは、指定された行の幅をピクセル単位で返します。行がコンテナの境界に達しない場合、maxWidthより小さくなることがあります。各行の幅がわかれば、手動で両端揃え効果を実装したり、短い行の横にインライン要素を配置したりできます。
特定の文字の位置を取得するには、getBoundingBox(offset)を使用します。これはleft、top、right、bottomの座標を持つRectを返します。これはカスタムテキストフィールドでのカーソル位置決めや、アニメーションによるテキスト範囲のハイライト表示に役立ちます。
TextMeasurerの最も強力なアプリケーションはカスタムLayoutコンポーネントで、テキストが他の要素と共存し、それらのサイズが相互に依存する場合です。たとえば、アイコンがブロック全体ではなくテキストの最初の行に対して中央揃えされるべき“アイコン+テキスト”ウィジェットです。
標準のRowでは、アイコンはRow全体の高さで中央揃えされますが、テキストが3行でアイコンが1行の場合、不自然に見えます。TextMeasurerを使用すると、最初の行を測定し、アイコンを正確にその行に揃えることができます。
val textMeasurer = rememberTextMeasurer()
Layout(
content = {
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "Multi-line text here")
},
measurePolicy = { measurables, constraints ->
val textMeasurable = measurables[1]
val textPlaceable = textMeasurable.measure(constraints)
val iconPlaceable = measurables[0].measure(constraints)
val firstLineHeight = textMeasurer.measure(
text = "Sample",
style = MaterialTheme.typography.bodyMedium,
constraints = Constraints(maxWidth = 2000, maxHeight = 100)
).size.height
layout(width = textPlaceable.width + iconPlaceable.width,
height = maxOf(textPlaceable.height, iconPlaceable.height)) {
iconPlaceable.placeRelative(0, (firstLineHeight - iconPlaceable.height) / 2)
textPlaceable.placeRelative(iconPlaceable.width, 0)
}
}
)
この例では、TextMeasurerを介した最初の行の測定がアイコンの位置を決定します。TextMeasurerがない場合、アイコンは複数行テキストの全体の高さで中央揃えされ、視覚的に不正確になります。このパターンはチャット、連絡先リスト、情報カードで広く使用されています。
TextMeasurerはUIスレッドで実行される同期APIです。過度に使用すると、特に弱いデバイスで大量のテキストを測定する場合にフレームドロップを引き起こす可能性があります。
最初のルール — 結果をキャッシュする。常にmeasure()呼び出しをテキストとスタイルの依存関係とともにrememberでラップしてください。CompositionLocal内やホットな再コンポジションパスでmeasure()を呼び出さないでください。
2番目のルール — maxWidthとmaxHeightを制限する。maxWidth = Constraints.Infinityに設定すると、TextMeasurerは行を折り返せず、テキストが画面外にはみ出します。特に複数行テキストの場合は、常に現実的な制約を渡してください。
3番目のルール — 同じパラメータでの繰り返し測定を避ける。テキストとスタイルが変更されていない場合、measure()の結果は同じになります。非同期シナリオでは、測定をDeferredまたはLaunchedEffectと組み合わせてください。
よくある質問
TextMeasurerはレンダリング前にテキストを測定し、onSizeChangedは後に測定します。TextMeasurerはテキストサイズが親レイアウトに影響する場合に必要です。onSizeChangedはコンポジション後のリアクティブロジックに適しています。
はい、TextMeasurer.measure()はAnnotatedStringを受け入れます。これにはインラインスタイル、リンク、色を含むSpannableStringが含まれます。すべてのスパンは測定時に考慮されます。
TextMeasurerはSkiaレンダリングレベルで動作し、すべての寸法がピクセル単位です。LocalDensity.currentを使用して変換します:density.run { width.toDp() }.
TextMeasurerはカスタムフォントが読み込まれるまでFontFamily.Defaultを使用します。正確な測定のためには、FontResourceを介してフォントの読み込みを待ち、async/awaitを使用してください。
各LazyColumnアイテムでテキストを測定すると、弱いデバイスでFPSが低下する可能性があります。最適化:rememberを介して結果をキャッシュし、テキストサイズを制限してください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。