Search code examples
androidkotlinandroid-jetpack-composeandroid-jetpackandroid-jetpack-compose-material3

How to prevent a gap between the keyboard and a bottom-aligned button in Android Jetpack Compose?


I want to have a button that is always at the bottom of the screen, when the keyboard opens it should go up with it and when it closes the button should go down again.

I tried different ways, with a Box wrapping everything and different configurations of nested Columns.

But sometimes after closing and opening the keyboard there is a gap between the keyboard and the button.

How do I fix that?

This is my current code:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun PlayerNameSelectionScreenContent(
    playerName: TextFieldValue,
    onChangePlayerName: (TextFieldValue) -> Unit = {},
    onContinueButtonClick: () -> Unit = {},
    navigateToNextScreen: () -> Unit = {},
    validateInput: () -> Boolean = { false },
    isInError: Boolean = false,
    errorMessage: String = ""
) {
    val focusManager = LocalFocusManager.current
    val focusRequester = remember { FocusRequester() }
    val hapticGenerator = LocalHapticFeedback.current
    val screenDescription = "Player Name Selection Screen"
    Column(
        modifier = Modifier
            .fillMaxSize()
            .imePadding()
            .semantics { contentDescription = screenDescription },
        verticalArrangement = Arrangement.SpaceBetween,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Column(
            modifier = Modifier
                .weight(0.40f, true)
                .fillMaxWidth(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Spacer(modifier = Modifier.size(32.dp))
            Text(
                text = "Player Setup",
                style = MaterialTheme.typography.displayMedium,
                textAlign = TextAlign.Center
            )
            Spacer(modifier = Modifier.size(16.dp))
            Text(
                text = "Player 1",
                style = MaterialTheme.typography.headlineLarge,
                textAlign = TextAlign.Center
            )
            Spacer(modifier = Modifier.size(8.dp))
            Text(
                text = "Start with yourself and proceed in storm order.",
                style = MaterialTheme.typography.bodyLarge,
                textAlign = TextAlign.Center
            )
        }

        Column(
            modifier = Modifier
                .weight(0.40f)
                .fillMaxWidth(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Spacer(modifier = Modifier.size(64.dp))
            Text(
                text = "Set the player name:",
                style = MaterialTheme.typography.headlineSmall,
                textAlign = TextAlign.Center
            )
            Spacer(modifier = Modifier.size(16.dp))
            val textFieldDescription = "Player name text field"
            OutlinedTextField(
                modifier = Modifier
                    .focusRequester(focusRequester)
                    .semantics {
                        contentDescription = textFieldDescription
                        setText { text ->
                            onChangePlayerName(TextFieldValue(text.text))
                            true
                        }
                        testTag = "PlayerName"
                        testTagsAsResourceId = true
                    },
                value = playerName,
                onValueChange = onChangePlayerName,
                label = { Text(stringResource(id = R.string.name_text)) },
                placeholder = { Text(stringResource(id = R.string.name_text)) },
                singleLine = true,
                isError = isInError,
                keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
                keyboardOptions = KeyboardOptions.Default.copy(
                    imeAction = ImeAction.Done,
                    keyboardType = KeyboardType.Text
                )
            )
            Spacer(modifier = Modifier.size(8.dp))
            val errorMessageDescription =
                stringResource(id = R.string.error_message_description)
            Crossfade(targetState = isInError, label = "isInErrorText") { isInError ->
                Text(
                    modifier = Modifier.semantics {
                        contentDescription = errorMessageDescription
                    },
                    text = if (isInError) errorMessage else "",
                    style = MaterialTheme.typography.bodyLarge,
                    textAlign = TextAlign.Center,
                    color = Color.Red,
                    minLines = 2
                )
            }
        }

        Column(
            modifier = Modifier
                .weight(weight = 0.20f, fill = true)
                .fillMaxWidth(),
            verticalArrangement = Arrangement.Bottom
        ) {
            Crossfade(targetState = isInError, label = "isInErrorButton") { isInError ->
                Button(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(48.dp),
                    shape = RectangleShape,
                    colors = if (isInError) {
                        ButtonDefaults.buttonColors(containerColor = Color.Red)
                    } else {
                        ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
                    },
                    onClick = {
                        hapticGenerator.performHapticFeedback(HapticFeedbackType.LongPress)
                        if (validateInput()) {
                            focusManager.clearFocus()
                            onContinueButtonClick()
                            navigateToNextScreen()
                        }
                    }
                ) {
                    Text(
                        modifier = Modifier.padding(horizontal = 16.dp),
                        text = "Continue",
                        style = MaterialTheme.typography.bodyLarge
                    )
                }
            }
        }
    }
    LaunchedEffect(key1 = Unit) {
        focusRequester.requestFocus()
        onChangePlayerName(TextFieldValue(playerName.text, TextRange(playerName.text.length)))
    }
}

Here is how it looks: Screen Screenshot

And here is a gif that shows what happens if I open and close the keyboard: Open Keyboard Gif

I tried wrapping everything with a box that has the modifier fillMaxSize and then a Column in it with Arrangement.Bottom in that configuration the gap between the keyboard and button could appear.

Then I tried a few different configurations of the mentioned code with different nestings of the Columns.

I also tried adding or removing the imePadding modifier on the different Boxes or Columns.


Solution

  • I have tried a bit more and think I found a solution:

    1. I have added android:windowSoftInputMode="adjustResize" to the activity section in the AndroidManifest.xml.
    <activity
        android:name="..."
        android:windowSoftInputMode="adjustResize"
        ... 
    </activity>
    
    1. I have three columns, I added the modifier .weight(0.50f) to the first two columns and removed the weight modifier from the last column.