記事の内容としては、テキストフィールドを押したときに出てくるキーボードの振る舞いについてです。
最近個人的にCompose Multiplatformでアプリを試していて、両OSのキーボード対応を調べました。
発端はMultiplatformですが、あまり整理できていなかったので、この機会にまとめようと思います。
keyboardOptionsと、keyboardActionsを扱った後に、キーボードを出したときにテキストフィールドを隠さない設定などを書きたいと思います。
keyboardOptions
KeyboardOptionsの各パラメータについての説明です。
| パラメータ | 説明 | 
|---|---|
| capitalization | 文字、単語、または文を自動的に大文字にするかどうかをキーボードに指示 | 
| autoCorrectEnabled | 自動修正を有効にするかどうかをキーボードに指示。KeyboardType.EmailとKeyboardType.Uriで有効 | 
| keyboardType | テキストフィールドで使用するキーボードタイプ。Unspecified, Text, Ascii, Number, Phone, Uri, Email, Password, NumberPassword, Decimalがある。数字だけ入力するキーボードなどを指定 | 
| imeAction | IME アクション。Unspecified, Default, None, Go, Search, Send, Previous, Next, Doneがある。リターンキーの見た目が変わり、挙動も変わる。 | 
| platformImeOptions | プラットフォーム固有の IME オプション | 
| showKeyboardOnFocus | デフォルトでtrueで、フォーカスしたときにキーボードを自動で表示する | 
| hintLocales | キーボードで言語切替をするときに、候補として表示させたいものを指定 | 
日本語圏に限定するなら、このなかではkeyboardTypeとimeActionをよく使うのではないかと思います。
iOSのswift ui開発なら、keyboardTypeとsubmitLabelというmodifierが相当するようです。
こちらどのようなキーボードになるか、視覚的に整理しておきましょう。
(OSや機種、設定等によって画像とは異なる結果になることもある点に注意)
| imeAction | Android | iOS | 
|---|---|---|
| ImeAction.Unspecified |  |  | 
| ImeAction.Default |  |  | 
| ImeAction.None |  |  | 
| ImeAction.Go |  |  | 
| ImeAction.Search |  |  | 
| ImeAction.Send |  |  | 
| ImeAction.Previous |  |  | 
| ImeAction.Next |  |  | 
| ImeAction.Done |  |  | 
keyboardActions
imeActionで、returnのようなキーを押したときにトリガーされる操作を実装できます。
たとえば、imeAction.Sendしたときに、リクエストを送るようなことができます。
キーボードでビューが隠れる問題への対応
何もしなったり設定を誤ると、キーボードが現れたときに、意図せずビューが上に行ってしまったり、キーボードで後ろのビューが隠れてしまいます。
そのときはmanifestのactivityに、android:windowSoftInputMode="adjustResize"を追加します。これがないと、背景のビューが上に行ってしまい、上の部分が隠れてしまうようです。
しかしこの設定だけでは不十分です。この設定だけだとキーボードで後ろのビューが隠れてしまいます。imePaddingという修飾子が用意されているので、これを使えばテキストフィールドがキーボードに隠れないように、キーボードが現れた時だけ自動でpaddingを追加することができます。
@Composable
fun KeyboardPlayground(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .imePadding()
            .verticalScroll(rememberScrollState()),
    ) {
        AllKeyboardTypeTextField()
        AllImeActionTextField()
    }
}
注意すべきは、imePaddingとverticalScrollの順番。上記を逆にすると、キーボードが現れた瞬間テキストフィールドはキーボードに隠れてしまいます。
なぜかというと、余白がどこに追加されるか考えると理解できます。正しいimePadding.verticalScrollの順番だと、キーボードの余白は表示したいコンテンツの外に配置されます。逆だと、スクロール対象のコンテンツの一番下に配置されます。
正しい順序だとキーボードが出現するとコンテンツの表示領域は狭まり、コンテンツはオフセットされるような形になりテキストフィールドは見えます。
正しくない逆の順序だと、キーボードが出現してもコンテンツの表示領域は変わりません。スクロール量はキーボードが現れたところで変わらないので、テキストフィールドが隠れてしまいます。
ここは言葉だけで説明できる自身がないので図を書いておきます。
(赤枠で囲ったものがフォーカス対象のテキストフィールドです。)
またimePaddingは外側にあるComposableに置いてもよいです。上記の場合だとKeyboardPlayground()の外側にScaffoldとかがあるなら、そちらに設定してもよいです。
Modifier.bringIntoViewRequester
Modifier.bringIntoViewRequesterを使って、ビューが表示領域に入るような実装を入れることができます。
なにかの条件が揃った時や、アクションがあったとき、自動でスクロールさせて特定のコンポーネントを表示させたいときに使います。
テキストフィールドフォーカスでテキストフィールドを表示したいような場合は前項のやり方で事足りるので、これを使わなくても良いと思います。
テキストフィールド以外でも対応できます。
以下は公式のサンプル
@Composable
fun BringIntoViewSample() {
    Row(Modifier.horizontalScroll(rememberScrollState())) {
        repeat(100) {
            val bringIntoViewRequester = remember { BringIntoViewRequester() }
            val coroutineScope = rememberCoroutineScope()
            Box(
                Modifier
                    // This associates the RelocationRequester with a Composable that wants to be
                    // brought into view.
                    .bringIntoViewRequester(bringIntoViewRequester)
                    .onFocusChanged {
                        if (it.isFocused) {
                            coroutineScope.launch {
                                // This sends a request to all parents that asks them to scroll so
                                // that this item is brought into view.
                                bringIntoViewRequester.bringIntoView()
                            }
                        }
                    }
                    .focusTarget()
            )
        }
    }
}
キーボード外の領域タップでキーボードを閉じたい時
親領域のComposableをフォーカス可能にして、FocusRequester.requestFocus()で、親にフォーカスを当てることで、テキストフィールドのフォーカスを外します。
indicationを指定しているのは、リップルエフェクトをなくすためです。
@Composable
fun KeyboardPlayground(modifier: Modifier = Modifier) {
    val focusRequester = remember { FocusRequester() }
    val interactionSource = remember { MutableInteractionSource() }
    Column(
        modifier = modifier
            .imePadding()
            .verticalScroll(rememberScrollState())
            .focusRequester(focusRequester)
            .focusable()
            .clickable(interactionSource = interactionSource, indication = null) {
                focusRequester.requestFocus()
            }
    ) {
        AllKeyboardTypeTextField()
        AllImeActionTextField()
    }
}
こちらの記事様(Jetpack Composeでも背景押したらTextFieldのフォーカスを外す - たくさんの自由帳)を参考にさせていただきました。
Compose multiplatformの注意点
これまで述べてきたものはCompose Multiplatformに対応していて、iOSでも同様の挙動が可能になります。
ただOSのキーボードの違いはあります。
iOSはnumberの場合にはreturnが表示されない、というものがありました。
こちらはOSのキーボードの仕様なので、注意しておきたいところです。
さいごに
いろいろな実装方法がありそうですが、自分のやり方をまとめてみました。
間違っているところがあればご指摘いただけると嬉しいです。




















