5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Jetpack ComposeでTextFieldにフォーカスした際にキーボードで隠れないようにする(BringIntoViewRequester)

Last updated at Posted at 2022-03-16

TextFieldでフォーカスした際にキーボードで入力欄が隠れてしまうことがあります。

横画面はもちろん、縦画面でもコンテンツ数が多い場合はよくある問題かと思います。
本記事ではBringIntoViewRequesterを用いた対応方法についてまとめました。

BringIntoViewRequesterとは?

For instance, you can call BringIntoViewRequester.bringIntoView 
to make all the scrollable parents scroll so that the specified item is brought into parent bounds.

引用元:BringIntoViewRequester

「親がスクロール可能な場合にbringIntoViewを呼び出すことにより、親をスクロールさせ指定されたアイテムが親の境界に入るように動作させることが出来ます」とのことです。

その為、含める際には対象のTextFieldだけではなく親Composeへの注意も必要になります。
スクロール可能にしておく事や、キーボード表示領域への対応も必要になります。

導入の前に注意点

BringIntoViewRequesterExperimentalFoundationApiに指定されております(2022年3月現在)

This foundation API is experimental and is likely to change or be removed in the future.

引用元:ExperimentalFoundationApi

引用元にも記載があるように実験的なものになっているため、将来的に変更または削除される可能性があります。
その為、プロダクトに導入する際は注意して下さい。

対応方法

TextFieldのBringIntoViewRequester対応

BringIntoViewRequesterを利用することで実現できます。
以前はRelocationRequesterを利用していましたが、こちらはdeprecatedになっています。

まず、スクロール先の指定になります。
スクロール対象のComposeModifierに対して
bringIntoViewRequesterRequesterを指定します。
これを行うことでRequesterに対して要求した際にこちらにスクロールするようになります。

次にスクロールの要求になります。
こちらはRequesterbringIntoViewを呼び出すことによりスクロールが実行されます。
TextFieldに対してフォーカスした際に発火させるにはonFocusEventを利用します。
onFocusEventStateFocusedの際にのみbringIntoViewを実行させています。

途中に含まれているdelayに関しましては本来必要無いのですが、
キーボードのアニメーションと被る場合に動作しなくなる為含めています。

Sample.kt
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BringIntoTextFiled() {
    val requester = remember { BringIntoViewRequester() }
    val coroutineScope = rememberCoroutineScope()
    var text by remember { mutableStateOf("hogehoge") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
            .padding(vertical = 1000.dp)
    ) {
        TextField(
            value = text,
            onValueChange = { text = it },
            modifier = Modifier
                .bringIntoViewRequester(requester)
                .onFocusEvent { focusState ->
                    if (focusState.isFocused) {
                        coroutineScope.launch {
                            delay(200)
                            requester.bringIntoView()
                        }
                    }
                }
        )
    }
}

キーボード表示領域に対応する

上記の該当箇所の対応のみでは上手く自動スクロールが出来ません。
理由としましてはキーボードが表示された際にActivityの表示領域とキーボードの表示領域が重なっており、
対象のTextFieldが領域外になっていないという判定の為です。

対応としてadjustResizeActivityに対して設定すると、
キーボードが表示された際にActivityの表示領域がキーボードの表示領域に合わせて変更される為、
上手く動作するようになります。

AndroidManifest.xml
<activity
    android:name=".SampleActivity"
    android:windowSoftInputMode="adjustResize"/>

これでTextFieldへのフォーカス時に自動スクロールされるようになりました。

LazyColumnを用いた場合の挙動について

LazyColumnを用いた場合ですが上手く動作させることが出来ませんでした。
フォーカスし、キーボードが表示された直後すぐにキーボードが非表示になってしまいました。
こちらについては後日調査&対応しようと思います。

入力欄が複数ある場合について

BringIntoViewRequesterTextFieldに対して個別に作成する必要があります。
以下のFailed Caseのように共通で利用した場合は想定した動作になりません。

✗ Failed Case

Failed Case.kt
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SampleField() {
    val requester = remember { BringIntoViewRequester() }
    val coroutineScope = rememberCoroutineScope()
    var text by remember { mutableStateOf("hogehoge") }

    TextField(
        value = text,
        onValueChange = {
            text = it
        },
    Column {
        TextField(
            value = "hogehage",
            onValueChange = {},
            modifier = Modifier
                .bringIntoViewRequester(requester)
                .onFocusEvent { focusState ->
                    if (focusState.isFocused) {
                        coroutineScope.launch {
                            delay(200)
                            requester.bringIntoView()
                        }
                    }
                }
        )
        TextField(
            value = "hogehage",
            onValueChange = {},
            modifier = Modifier
                .bringIntoViewRequester(requester)
                .onFocusEvent { focusState ->
                    if (focusState.isFocused) {
                        coroutineScope.launch {
                            delay(200)
                            requester.bringIntoView()
                        }
                    }
                }
        )
    }
}

○ Success Case

SuccessCase.kt
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SampleField() {
    val requester1 = remember { BringIntoViewRequester() }
    val requester2 = remember { BringIntoViewRequester() }
    val coroutineScope = rememberCoroutineScope()

    Column {
        TextField(
            value = "hogehage",
            onValueChange = {},
            modifier = Modifier
                .bringIntoViewRequester(requester1)
                .onFocusEvent { focusState ->
                    if (focusState.isFocused) {
                        coroutineScope.launch {
                            delay(200)
                            requester1.bringIntoView()
                        }
                    }
                },
        )
        TextField(
            value = "hogehage",
            onValueChange = {},
            modifier = Modifier
                .bringIntoViewRequester(requester2)
                .onFocusEvent { focusState ->
                    if (focusState.isFocused) {
                        coroutineScope.launch {
                            delay(200)
                            requester2.bringIntoView()
                        }
                    }
                },
        )
    }
}

上記のSuccess Caseも冗長なので1つのComponentとしてまとめ使い回せるようにしましょう。

以上

5
3
1

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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?