0

Update

I awarded already a bounty to the answer I am currently using, however, if there is a native/better way to handle this, I will award that person a bounty of 500 points.

Problem

I have been struggling with this problem for awhile, I checked a lot of similar questions, but I never got it working. What I want is pretty straightforward, considering this requirements:

  • Jetpack Compose is the root view
  • Views are wrapped inside a Surface -> Scaffold -> content in a Bottom Bar
  • The keyboard is inside an AndroidView, I don't think it matters though

Goal

So there is a TopAppBar and Bottom Bar. When the keyboard appears, it should just slide OVER the Bottom Bar and ofcourse the TopAppBar should be visible.

Result

Without doing any configuration in the Manifest file, this is the result (TopAppBar is hidden):

enter image description hereenter image description here

When using the famous adjustResize mode, the Bottom Bar will be on top of the keyboard:

enter image description here

I tried adjustPan as well, the TopAppBar will be hidden.

Code

A full reproduction project is available at: https://github.com/Jasperav/JetpackComposeNavigation, this is the relevant code:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Screen() {
    val items = listOf(
        Triple("a", Icons.Default.Person, Icons.Filled.Person),
        Triple("b", Icons.Default.Notifications, Icons.Filled.Notifications),
    )
    var selectedTab = items[0]
    val navHostController = rememberNavController()

    Scaffold(
        bottomBar = {
            NavigationBar {
                items.forEachIndexed { index, item ->
                    selectedTab = item

                    val isSelected = index == items.indexOf(selectedTab)

                    NavigationBarItem(
                        icon = {
                            Icon(
                                if (isSelected) item.second else item.third,
                                contentDescription = null
                            )
                        },
                        label = { Text(text = item.first) },
                        selected = isSelected,
                        onClick = {
                            navHostController.navigate(item.first) {
                                popUpTo(navHostController.graph.findStartDestination().id) {
                                    saveState = true
                                }

                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    ) {
        NavHost(
            navHostController,
            startDestination = items[0].first,
            Modifier.padding(it)
        ) {
            composable(items[0].first) {
                Scaffold(topBar = {
                    TopAppBar(
                        title = {
                            Text(text = "Text",
                            )
                        },
                    )
                }) {
                    AndroidView(modifier = Modifier.padding(it).fillMaxSize(), factory = { context ->
                        val constraintLayout = ConstraintLayout(context)

                        constraintLayout.setBackgroundColor(context.getColor(android.R.color.holo_red_dark))
                        val editText = EditText(context)
                        editText.setText("Click here")

                        editText.id = View.generateViewId()

                        constraintLayout.addView(editText)

                        val constraintSet = ConstraintSet()

                        constraintSet.clone(constraintLayout)

                        constraintSet.connect(editText.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
                        constraintSet.connect(editText.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
                        constraintSet.connect(editText.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)

                        constraintSet.applyTo(constraintLayout)

                        constraintLayout
                    })
                }
            }
            composable(items[1].first) {
                Column {
                    Text("Second")
                    Button(onClick = {
                        navHostController.navigate(
                            "nested/" + UUID.randomUUID().toString()
                        )
                    }) {
                        Text(text = "Go to nested")
                    }
                }
            }
            composable("nested/{id}") {
                Text("nested")
            }
        }
    }
}
J. Doe
  • 12,159
  • 9
  • 60
  • 114

2 Answers2

1

We also faced same kind of issue in our live project and ended up with nothing. We had 2 options at the end, which are:

  • Hide bottom bar when keyboard is opened
  • Hide bottom bar directly while navigating to the screen where keyboard is used

We opted for 2nd workaround and below is sample implementation:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        HomeScreen()
    }
}

@Composable
fun HomeView() {

    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val bottomBarState = rememberSaveable { (mutableStateOf(true)) }
    val nonBottomBarRequiredRoute = listOf(
        "Screen X,
        "Screen Y,
        ...
    )
    val currentRoute =
        navBackStackEntry?.destination?.route?.substringBefore("?")?.substringBefore("/")
    bottomBarState.value = (currentRoute !in nonBottomBarRequiredRoute)
    val contextAppCompat = LocalContext.current as AppCompatActivity

    when (route) {
        in nonBottomBarRequiredRoute ->
            contextAppCompat.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)

        else ->
            contextAppCompat.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
    }

    Scaffold(
        bottomBar = {
            AnimatedBottomBar(
                navController,
                bottomBarState = bottomBarState
            )
        }
    ) {
        //content
    }
}

@Composable
fun AnimatedBottomBar(
    navController: NavHostController,
    bottomBarState: MutableState<Boolean>
) {
    if (bottomBarState.value) {
        //bottom bar code
    }
}

You could also use 1st case where you hide bottom bar only if keyboard is open, but trust me, animation will look really bad when bottom bar visibility toggles when keyboard opens or closes!

Here is the link for knowing keyboard state: https://stackoverflow.com/a/69533584/19212377

Sample implementation: ![link]

Megh
  • 831
  • 2
  • 12
  • It's crazy Android does not have a modifier or something to just hide the bottom bar... How do you animate the bottom bar? – J. Doe Jul 05 '23 at 08:15
  • Using AnimatedVisibility, we can add animations, but it will look awkward to users because it basically starts exit animation when keyboard is opened. Check this link: https://ibb.co/sV4d1hR – Megh Jul 05 '23 at 12:04
  • Updated link: https://drive.google.com/file/d/1biu6NwZTrBx8A0lwMXzkvzykcW-X3dFk/view?usp=sharing – Megh Jul 06 '23 at 03:26
  • I awarded you the bounty since I am using this way to hide the keyboard conditionally, but I still believe there is another way how to handle it though – J. Doe Jul 08 '23 at 10:11
0

Keyboards work in 2 ways- resize or pan. In resize, the app is drawn in the remaining space. In pan, the app is scrolled such that the cursor remains on screen, but anything else may go off. Scaffold puts a top and bottom bar on the screen. So in resize mode, which sizes your app to the remaining space, scaffold will always keep both bars on screen. In pan mode, the top bar will be scrolled off if needed to keep the cursor on screen.

Really getting exactly what you want will be hard, because that's not how Android works. Resize will ensure the top content stays on screen, but there is no mode of raising the keyboard that promises anything about the bottom content. Except adjustNone, which doesn't do any adjustment. But that mode is never recommended because it can cause the cursor to be covered and the use can't see what they're typing.

My real suggestion is to change your requirement here. You can spend dozens of hours fighting the keyboard system here, eventually get something that kind of works on most screen sizes, and then it breaks the minute your change your UI. Android just does not give you that kind of control over the keyboard.

Gabe Sechan
  • 90,003
  • 9
  • 87
  • 127