ComposeのTextField がキーボードに隠れてしまう問題:原因分析と解決方法
Jetpack ComposeのTextFieldがキーボードに隠れてカーソルが見えなくなる問題はよく発生します。
特に複数行入力では入力位置を確認できず、UXに大きな影響があります。
ComposeのBasicTextFieldは自動スクロールやIME対応を提供していないため、
この問題を解決するにはカーソル位置に応じてスクロールを制御する処理を自前で実装する必要があります。
本記事では、この問題がなぜ起きるのか、そして
Composeで実装したカスタムTextFieldによる解決方法を紹介します。
1. 画面構成
val textFieldState = rememberTextFieldState("")
Column(
modifier = Modifier.fillMaxSize()
) {
AppBar(...)
CustomTextField(
textFieldState = textFieldState,
modifier = Modifier.weight(1f),
...
)
BottomSection(
modifier = Modifier.weight(1f),
...
)
}
上下をweight(1f)で1:1に分割し、
上側に複数行対応のTextField(BasicTextFieldをラップしたCustomTextField)を配置しています。

2. 正常に動作するケース — TextFieldの高さに全行が収まっている場合
次は、テキストを11行まで入力した状態です。
この場合、すべての行がTextFieldの高さに収まるため、スクロールは不要です。

この状態で任意の行をタップすると、
キーボードが表示されながらタップした行に正しくフォーカスが当たります。
- キーボードを表示
-
TextFieldの高さが縮む - 縮んだ領域内でもカーソルが正常に見える
-
TextFieldが隠れない
つまり、テキストが“画面内に収まっている”状態では、
Composeの標準動作だけでキーボードとTextFieldが正常に連携します。
このため、この構成だけを見ると
「特に問題はなさそうだ」と思ってしまいがちです。
しかし、テキストがさらに増えてTextFieldの高さを超え始めると
ここから問題が発生します → 次のセクションで解説します。
3. 問題が発生するケース — TextFieldの高さを超えた場合
テキストが増えてTextFieldの表示領域を超えるほど複数行になった瞬間、
Composeの標準動作だけではフォーカスが正しく維持されなくなります。
以下は、テキストがTextFieldの高さを超えて
スクロールが必要な状態で任意の行をタップしたときの動作です。

この状況では次の問題が発生します:
- 行をタップするとキーボード自体は表示される
- しかしカーソルがタップした位置へ移動しない
- 画面も適切な位置までスクロールしない
- 結果として
TextFieldの下部がキーボードに完全に隠れてしまう
つまり、テキストがTextFieldの高さに収まらなくなった時点で、
Compose標準の動作では 「カーソル位置の可視性」 が維持できなくなります。
ユーザー視点では、
「今どこを入力しているのか分からない」
という状態になり、UXに大きな問題を引き起こします。
この問題を解決するには、
カーソル位置を取得し、その位置が常に可視領域に入るようにスクロールを制御する処理
を自前で実装する必要があります。
4. なぜこの問題が起きるのか
この問題の本質的な原因は、ComposeのBasicTextFieldがカーソルの可視性を保証しないことにあります。
BasicTextFieldは
- カーソルが画面外に出たかどうかを判断せず
- 出た場合にスクロールで追従する仕組みもなく
- IME(キーボード)領域も自動で考慮しません。
テキストがTextField内に収まっている間は問題ありませんが、
行数が増えてTextField内でスクロールが必要になった瞬間から、
カーソルが追従しなくなり、キーボードに隠れてしまいます。
つまり、これはComposeの自動スクロール非対応という構造的制限が原因です。
5. 解決方法 — カーソル位置に応じて自動スクロールするCustomTextField
この問題を解決するためには、
カーソルが常に画面内に見えるようにスクロールを制御するTextField
を自前で実装する必要があります。
解決のポイントは次の4つです。
- onTextLayoutからTextLayoutResultを取得し、カーソル位置(getCursorRect())を計算
- TextFieldの表示領域(top/bottom)を把握
- カーソルがその領域をはみ出した場合、必要量だけScrollStateを移動
- IME(キーボード)の高さも考慮して可視範囲を計算
以下は実際の CustomTextField の実装コードです。
@Composable
fun CustomTextField(
textFieldState: TextFieldState,
modifier: Modifier = Modifier,
) {
// TextLayoutResult は現在の文字レイアウト情報(カーソル位置取得に必要)
val latestLayout = remember { mutableStateOf<TextLayoutResult?>(null) }
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
var textFieldHeight by remember { mutableIntStateOf(0) }
val imeBottom = WindowInsets.ime.getBottom(LocalDensity.current)
...
// カーソルの上下に少し余白をつけるための margin
val margin = with(LocalDensity.current) { textStyle.lineHeight.toPx() * CURSOR_VERTICAL_MARGIN_RATIO }
LaunchedEffect(imeBottom, latestLayout.value) {
val layout = latestLayout.value ?: return@LaunchedEffect
// キーボードが閉じているときは処理しない
if (imeBottom <= 0) return@LaunchedEffect
// 現在のカーソル位置(オフセット)を取得
val textLength = layout.layoutInput.text.length
val offset = textFieldState.selection.max.coerceIn(0, textLength)
// カーソルの位置(Rect)を取得
val cursorRect = layout.getCursorRect(offset)
// TextField の現在の可視領域(スクロール込み)
val visibleTop = scrollState.value.toFloat()
val visibleBottom = scrollState.value + textFieldHeight
// スクロールすべき位置を判断
val targetScroll = when {
// カーソルが下方向の可視領域から外れた場合
cursorRect.bottom > visibleBottom - margin -> {
val diff = cursorRect.bottom - (visibleBottom - margin)
scrollState.value + diff
}
// カーソルが上方向の可視領域から外れた場合
cursorRect.top < visibleTop + margin -> {
val diff = (visibleTop + margin) - cursorRect.top
scrollState.value - diff
}
// すでに可視範囲内 → スクロール不要
else -> null
}
// スクロールが必要な場合、アニメーション付きで移動
if (targetScroll != null) {
coroutineScope.launch {
scrollState.animateScrollTo(
targetScroll.toInt().coerceIn(0, scrollState.maxValue)
)
}
}
}
BasicTextField(
state = textFieldState,
scrollState = scrollState,
onTextLayout = { latestLayout.value = it },
modifier = modifier
.onSizeChanged { textFieldHeight = it.height }
...
}
実装のポイント
- getCursorRect()でカーソル位置を取得
- カーソルが可視領域に入っているかを常にチェック
- 見えていない場合だけスクロールを移動
- 上方向・下方向のどちらにも対応し、安定性が高い
適用結果
以下は、テキストがTextFieldの高さを超えても
カーソルが常に画面内に維持される動作例です。

6. まとめ
Jetpack ComposeのBasicTextFieldは、
複数行入力においてカーソルの可視性維持やIME対応 を標準で提供していないため、
行数が増えた瞬間に問題が発生しやすい仕組みになっています。
本記事では、
- なぜカーソルがキーボードに隠れてしまうのか
-
TextFieldが抱える構造的な制限 - カーソル位置ベースでスクロール制御が必要な理由
- そして、それを解決する
CustomTextFieldの実装
について解説しました。
今回のアプローチには次のようなメリットがあります。
- どのような状況でも カーソルが常に画面内に見える
- キーボードの表示・非表示に左右されない安定した入力体験
- Composeの標準APIだけで実装可能
- 他の入力画面にも簡単に再利用できる
複数行入力を扱うCompose画面では、
このような カーソル可視性を維持する仕組み が欠かせません。
同じ問題に悩んでいる方の参考になれば幸いです
Discussion