1

I'm developing a typical chat screen, with the text input at the bottom. The app has a bottom bar with tabs and a topbar.

I'm struggling at getting what I understand to be the expected behavior (Android, iOS):

  1. When keyboard is opened, text input is (directly) on top of keyboard.
  2. The topbar doesn't move, i.e. stays at top of screen.
  3. Bottom bar is visible at bottom of screen when keyboard is closed.

I've tried different combinations of using scaffold's innerPadding, applying imePadding() to different elements, or not applying it, etc. There's always a different issue that doesn't fulfill the 3 points above. E.g. topbar is pushed out of screen, there's space with size of tabbar between input and keyboard. Sometimes there are platform specific differences.

Are my expectations correct? if yes, what's wrong with the code? if not, what to change?

Here's a repo with this code: https://github.com/ivnsch/textfieldpadding/tree/6717e4d247970316bcc36d52427de408514cff83

enter image description here

Issue example when opening keyboard (current code):

enter image description here

Commenting imePadding(), space goes away, now topbar is pushed out:

enter image description here

package foo.bar.inputtestdemo

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.ui.tooling.preview.Preview

@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
fun App() {
    Scaffold(
        topBar = {
            Column {
                TopAppBar(title = { Text("topbar") })
            }
        },
        bottomBar = { TabsBar() }
    ) { innerPadding ->

//      Box(modifier = Modifier.fillMaxSize()) {
        Box(modifier = Modifier.padding(innerPadding)) {
//      Box {
            Contents()
        }
    }
}

@Composable
private fun Contents() {
    val focusManager = LocalFocusManager.current

    Box {
        Column(
            modifier = Modifier
                .imePadding()
                .fillMaxSize()
                .pointerInput(Unit) {
                    detectTapGestures(onTap = {
                        focusManager.clearFocus()
                    })
                },
        ) {
            Box(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth(),
                contentAlignment = Alignment.Center
            ) {
                Text("Main content area")
            }

            BasicText()
        }
    }
}

@Composable
private fun BasicText() {
    var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue("..."))
    }

    BasicTextField(
        value = textState,
        onValueChange = { textState = it },
        modifier = Modifier
            .height(60.dp)
            .fillMaxWidth()
            .background(Color.Yellow),
    )
}

@Composable
fun TabsBar() {
    NavigationBar(
        modifier = Modifier.height(100.dp),
        containerColor = Color.Green,
    ) { }
}

1 Answer 1

0

You currently have two overlapping layout "padding handlers" that fight each other.

  • Scaffold already applies WindowInsets, including navigation and IME insets, via its innerPadding.

  • You're also manually applying .imePadding() to your content column.

This double application leads to

  • extra space between textfield and keyboard, or

  • top bar moving out of view (depending on which element receives imePadding).

Essentially, Compose is overcompensating for the keyboard height.

You want exactly one component in the hierarchy to handle imePadding() and you want to control when the bottom bar hides.

Below is the reliable approach (works on Android, Compose Multiplatform, and usually iOS too)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview
fun App() {
    Scaffold(
        topBar = { TopAppBar(title = { Text("topbar") }) },
        bottomBar = {
            // Bottom bar only shown when keyboard closed
            if (!isKeyboardOpen()) {
                TabsBar()
            }
        }
    ) { innerPadding ->
        // Important: apply innerPadding (navigation + system bars)
        // and add imePadding() only once here.
        Box(
            modifier = Modifier
                .padding(innerPadding)
                .imePadding() // ensures input moves above keyboard
                .fillMaxSize()
        ) {
            Contents()
        }
    }
}

Then your Contents() can be simple

@Composable
private fun Contents() {
    val focusManager = LocalFocusManager.current

    Column(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectTapGestures(onTap = { focusManager.clearFocus() })
            },
    ) {
        Box(
            modifier = Modifier
                .weight(1f)
                .fillMaxWidth(),
            contentAlignment = Alignment.Center
        ) {
            Text("Main content area")
        }

        BasicText()
    }
}

And your BasicText() as-is is fine.


You can detect keyboard visibility using WindowInsets.ime which you can use for hiding bottom bar.

@Composable
fun isKeyboardOpen(): Boolean {
    val ime = WindowInsets.ime
    val imeVisible = ime.getBottom(LocalDensity.current) > 0
    return imeVisible
}

So your final structure should be

Scaffold(
    topBar = { TopAppBar(...) },
    bottomBar = { if (!isKeyboardOpen()) TabsBar() }
) { innerPadding ->
    Box(
        Modifier
            .padding(innerPadding)
            .imePadding()
            .fillMaxSize()
    ) {
        Contents()
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.