1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Jetpack Compose】IME表示時やテキストの編集時に、カーソルが表示領域に収まるようにTextFieldを自動スクロールする

Last updated at Posted at 2025-09-17

IME表示時にTextFieldをスクロールしたい

 ComposeのTextFieldはテキストを編集したときに、カーソルが表示領域に収まるように自動スクロールをしてくれます。
 しかし画面いっぱいに広がっているようなTextFieldの場合、IMEの表示などでTextFieldの表示サイズが小さくなって、カーソルがIME裏に隠れたとしても、自動でカーソルを表示領域内に移動はしてくれません↓

IME表示前 IME表示後
Qiita_自動スクロール_修正前_IME表示前.png Qiita_自動スクロール_修正前_IME表示後.png

 そのためIMEに合わせてスクロールする && 既存のTextFieldのように編集時にもスクロールするTextFieldを作りたかった訳です。
 一応AndroidViewでEditTextScrollViewを組み合わせるか、RelocationRequesterを使用する方法などでも実装は可能かと思いますが、今回はComposeのTextFieldにあの手この手で、IME表示時とテキストの編集時にカーソルが表示領域内に自動スクロールするようなTextFieldを実装しました。

実装

Column(
    modifier = modifier
        .background(color = Color.White)
        .systemBarsPadding()
        .imePadding(),
    horizontalAlignment = Alignment.CenterHorizontally,
) {
    Text(
        text = "上のView",
        modifier = Modifier.padding(bottom = 20.dp),
        color = Color.Black,
    )
    
    val scrollState = rememberScrollState()
    var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
    var textFieldHeightPx by remember { mutableIntStateOf(0) }
    var textFieldValue by remember { mutableStateOf(TextFieldValue()) }
    
    LaunchedEffect(textFieldHeightPx, textFieldValue.selection.end) {
        val layoutResult = textLayoutResult ?: return@LaunchedEffect
        val cursorLine = layoutResult.getLineForOffset(textFieldValue.selection.end)
        val cursorBottom = layoutResult.getLineBottom(cursorLine).roundToInt()
        val cursorTop = layoutResult.getLineTop(cursorLine).roundToInt()
        val scrollPosition = when {
            cursorBottom > scrollState.value + textFieldHeightPx -> cursorBottom - textFieldHeightPx
            cursorTop < scrollState.value -> cursorTop
            else -> null
        }
        scrollPosition?.let { scrollState.scrollTo(it.coerceIn(0, scrollState.maxValue)) }
    }

    BasicTextField(
        value = textFieldValue,
        modifier = Modifier
            .weight(1f)
            .fillMaxSize()
            .background(color = Color.Gray)
            .onSizeChanged { textFieldHeightPx = it.height }
            .verticalScroll(state = scrollState),
        onValueChange = { textFieldValue = it },
        onTextLayout = { textLayoutResult = it },
    )
        
    Text(
        text = "下のView",
        modifier = Modifier.padding(top = 20.dp),
        color = Color.Black,
    )
}

動作

 ↑の実装で、表示領域外の文章が編集されたときやIMEの表示に合わせてTextFieldが自動でスクロールします。

表示領域外の文章が編集された時 IME表示後
Qiita_自動スクロール_修正後_下スクロール.png Qiita_自動スクロール_修正後_上スクロール.png

解説

 まずLaunchedEffectで、TextFieldの表示領域の高さ(textFieldHeightPx)が変更された時と、選択部分の末尾(textFieldValue.selection.end)が変更された時に自動スクロールを実行するようにします。
 これでTextFieldが小さくなってカーソルが隠れたり、カーソルが隠れた状態で文字が入力されたりした時に自動でスクロールされます。
 スクロールの処理は以下のような単純な引き算です↓

  • カーソルの最上部、最下部の位置をlayoutResultからそれぞれ計算
    -> 現在のスクロール位置がカーソルの最上部より下ならば、カーソルの最上部にスクロール
    -> 現在のスクロール位置 + TextFieldの表示領域の高さがカーソルの最下部より上にならばカーソルの最下部 + TextFieldの表示領域の高さの位置(TextFieldの表示領域の一番下にカーソルの最下部が合う位置)にスクロール
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?