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.
「親がスクロール可能な場合にbringIntoViewを呼び出すことにより、親をスクロールさせ指定されたアイテムが親の境界に入るように動作させることが出来ます」とのことです。
その為、含める際には対象のTextFieldだけではなく親Composeへの注意も必要になります。
スクロール可能にしておく事や、キーボード表示領域への対応も必要になります。
導入の前に注意点
BringIntoViewRequesterはExperimentalFoundationApiに指定されております(2022年3月現在)
This foundation API is experimental and is likely to change or be removed in the future.
引用元にも記載があるように実験的なものになっているため、将来的に変更または削除される可能性があります。
その為、プロダクトに導入する際は注意して下さい。
対応方法
TextFieldのBringIntoViewRequester対応
BringIntoViewRequesterを利用することで実現できます。
以前はRelocationRequesterを利用していましたが、こちらはdeprecatedになっています。
まず、スクロール先の指定になります。
スクロール対象のComposeのModifierに対して
bringIntoViewRequesterでRequesterを指定します。
これを行うことでRequesterに対して要求した際にこちらにスクロールするようになります。
次にスクロールの要求になります。
こちらはRequesterのbringIntoViewを呼び出すことによりスクロールが実行されます。
TextFieldに対してフォーカスした際に発火させるにはonFocusEventを利用します。
onFocusEventでStateがFocusedの際にのみbringIntoViewを実行させています。
途中に含まれているdelayに関しましては本来必要無いのですが、
キーボードのアニメーションと被る場合に動作しなくなる為含めています。
@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が領域外になっていないという判定の為です。
対応としてadjustResizeをActivityに対して設定すると、
キーボードが表示された際にActivityの表示領域がキーボードの表示領域に合わせて変更される為、
上手く動作するようになります。
<activity
android:name=".SampleActivity"
android:windowSoftInputMode="adjustResize"/>
これでTextFieldへのフォーカス時に自動スクロールされるようになりました。

LazyColumnを用いた場合の挙動について
LazyColumnを用いた場合ですが上手く動作させることが出来ませんでした。
フォーカスし、キーボードが表示された直後すぐにキーボードが非表示になってしまいました。
こちらについては後日調査&対応しようと思います。
入力欄が複数ある場合について
BringIntoViewRequesterはTextFieldに対して個別に作成する必要があります。
以下のFailed Caseのように共通で利用した場合は想定した動作になりません。
✗ Failed Case
@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
@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としてまとめ使い回せるようにしましょう。
以上