Entering data is an important task in many apps. On devices with no physical keyboard (the vast majority in Android land) a so-called soft(ware) keyboard handles user input. Now, you may be wondering why we need to talk about these virtual peripherals at all. Shouldn't the operating system take care? I mean, in terms of user interface, the app expresses its desire to allow user input by showing and configuring an editable text field. What else needs to be done? This article takes a closer look at how Jetpack Compose apps interact with the keyboard.
Let's start with a simple Compose hierarchy:
@Composable
fun KeyboardHandlingDemo1() {
var text by remember { mutableStateOf(TextFieldValue()) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom
) {
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
.background(color = MaterialTheme.colors.primary)
.weight(1.0F),
contentAlignment = Alignment.BottomCenter
) {
Text(
modifier = Modifier.padding(bottom = 16.dp),
text = stringResource(id = R.string.app_name),
color = MaterialTheme.colors.onPrimary,
style = MaterialTheme.typography.h5
)
}
TextField(modifier = Modifier.padding(bottom = 16.dp),
value = text,
onValueChange = {
text = it
})
}
}
Looks good, right? Now, let's see what happens if the text field gets focus.
This surely doesn't look terrible, but it isn't great, either. As the text field will likely have the attention of the user, it should be visible fully, right? Here, it's important to understand how the soft keyboard interacts with the activity and the window the activity is displayed in. There has almost always (since API level 3) been a manifest attribute for this, windowSoftInputMode
. It belongs to <activity />
.
controls how the main window of the activity interacts with the window containing the on-screen soft keyboard.
There are two main aspects:
- Should the soft keyboard be visible when the activity becomes the focus of user attention?
- Which adjustment should be made to the activity's main window when a part of the window is covered by the soft keyboard?
In this article, I'll focus on the latter one. For a general introduction, please refer to Handle input method visibility.
Now, let's look at adjustment-related values.
adjustUnspecified
is the default setting for the behavior of the main window. The doc says:
It is unspecified whether the activity's main window resizes to make room for the soft keyboard, or whether the contents of the window pan to make the current focus visible on-screen. The system will automatically select one of these modes depending on whether the content of the window has any layout views that can scroll their contents. If there is such a view, the window will be resized, on the assumption that scrolling can make all of the window's contents visible within a smaller area.
adjustResize
:
The activity's main window is always resized to make room for the soft keyboard on screen.
adjustPan
:
The activity's main window is not resized to make room for the soft keyboard. Rather, the contents of the window are automatically panned so that the current focus is never obscured by the keyboard and users can always see what they are typing. This is generally less desirable than resizing, because the user may need to close the soft keyboard to get at and interact with obscured parts of the window.
If you recall the last screenshot, the window obviously is not resized. So, ...
The system will automatically select one of these modes depending on whether the content of the window has any layout views that can scroll their contents.
... seems to not work well for Compose apps. Which is certainly not surprising, as the root view of a Compose hierarchy being displayed using setContent { ... }
is ComposeView
, which extends AbstractComposeView
, which in turn extends ViewGroup
(which can't scroll).
So, the fix is simple: just add
android:windowSoftInputMode="adjustResize"
to your <activity />
tag.
Multiple text fields
Let's look at another Compose hierarchy:
@Composable
fun KeyboardHandlingDemo2() {
val states = remember {
mutableStateListOf("1", "2", "3",
"4", "5", "6", "7", "8", "9", "10")
}
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
itemsIndexed(states) { i, _ ->
OutlinedTextField(value = states[i],
modifier = Modifier.padding(top = 16.dp),
onValueChange = {
states[i] = it
},
label = {
Text("Text field ${i + 1}")
})
}
}
}
The user interface contains quite a few editable text fields. However, with the above implementation, the user cannot move to the next field using the soft keyboard, but must click and scroll. Fortunately, we can achieve this easily using Compose keyboard actions and options. The following lines are added to the call to OutlinedTextField()
:
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
onNext = {
focusManager.moveFocus(FocusDirection.Down)
}
),
We configure the soft keyboard to display a special Next key (ImeAction.Next
) and use a FocusManager
(which belongs to package androidx.compose.ui.focus
) inside the onNext
callback of KeyboardActions
to navigate (move the focus) to the (vertically) next text field.
val focusManager = LocalFocusManager.current
Cool, right? There is one more thing we need to do, though. Our text fields belong to a scrollable list. Moving focus does not change the portion of the list that is currently visible. Here's how to do that:
listState.animateScrollToItem(i)
animateScrollToItem()
is a suspend function, so it should be called from a coroutine or another suspend function.
coroutineScope.launch {
listState.animateScrollToItem(i)
}
Finally, to get a coroutine scope in a composable function, you can use rememberCoroutineScope()
:
val coroutineScope = rememberCoroutineScope()
There's one more thing I'd like to show you: how to close the software keyboard.
Showing and hiding the soft keyboard
The following screenshot shows my KeyboardHandlingDemo3()
composable function. It allows the user to enter a number and computes its square after the Calculate button or the special Done key on the soft keyboard was pressed. What you can't see on the screenshot: the soft keyboard is closed. This may be desirable to again show the complete user interface after the data has been input.
Let's look at the code:
@ExperimentalComposeUiApi
@Composable
fun KeyboardHandlingDemo3() {
val kc = LocalSoftwareKeyboardController.current
var text by remember { mutableStateOf("") }
var result by remember { mutableStateOf("") }
val callback = {
result = try {
val num = text.toFloat()
num.pow(2.0F).toString()
} catch (ex: NumberFormatException) {
""
}
kc?.hide()
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Row {
TextField(modifier = Modifier
.padding(bottom = 16.dp)
.alignByBaseline(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
callback()
}
),
value = text,
onValueChange = {
text = it
})
Button(modifier = Modifier
.padding(start = 8.dp)
.alignByBaseline(),
onClick = {
callback()
}) {
Text(stringResource(id = R.string.calc))
}
}
Text(
text = result,
style = MaterialTheme.typography.h4
)
}
}
The computation takes place in the callback
lambda. Here, the soft keyboard is closed, too, by invoking hide()
on a LocalSoftwareKeyboardController
instance. Please note that this API is experimental and may change in future Compose versions.
We configure a number pad with a Done key by passing keyboardType = KeyboardType.Number
and imeAction = ImeAction.Done
to KeyboardOptions()
. The callback
lambda is invoked from the onClick
callback of the button and inside onDone
, which belongs to KeyboardActions()
.
Conclusion
In this article I showed you how to interact with the soft keyboard in Compose apps. Did I miss something? Would you like a follow up? Kindly share your thoughts in the comments.
Top comments (1)
What about using inset padding to account for the keyboard inset and move the view up?