I have a simple view like this. In the emulator, when I press Tab on the keyboard, nothing happens. There's no indication that the button in focused, and the onClick is not called when I press Enter.
NOTE: This is for accessibility, i.e., those who cannot use their fingers to tap and need an indicator that the button has focus, or those who use the app in a tablet + keyboard.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
Button(onClick = { println("clicked") }) {
Text(text = "A Button")
}
}
}
}
}
Please help. The Compose version is 1.0.5.
By the way,
I've read almost all pages of Compose documentation in Android Developers, including Accessibility in Compose, but I cannot find any useful information that solves my simple problem.
Compose TextField could receive focus either by pressing Tab or arrow keys.
I've also tried to place a View Button and a Compose Button side by side, only ViewButton could receive focus.
Update
val iSource = remember { MutableInteractionSource() }
val focused by iSource.collectIsFocusedAsState()
val pressed by iSource.collectIsPressedAsState()
Button(
onClick = { println("clicked") },
Modifier.focusable(interactionSource = iSource),
) {
Text(text = "A Button (focused=${focused}, pressed=${pressed})")
}
This way, "focused" become true when I navigate by using arrow keys (Up/Down), but:
There's no focused indication.
Pressing Tab still doesn't work.
When it's focused, pressing Space or Enter doesn't trigger the onClick listener.
Related
As shown above, the list of items, the text input field and the add button go up when the user open the keyboard,
I want the list of items to stay in position while the text input field and the add buton go up as it does.
code:
Activity:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
OlegarioLopezTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) { Navigation() }
}
}
}
The Navigation() func just call the Composable
Composable:
#Composable
fun ListScreen(
viewModel: MainScreenViewModel,
navController: NavController
) {
LazyColumn{...}
MainTextField(viewModel)
AddButton(viewModel)
}
Ensure that the activity's windowSoftInputMode is set to adjustResize:
<activity
android:name=".MyActivity"
android:windowSoftInputMode="adjustResize">
</activity>
In this way the activity's main window is always resized to make room for the soft keyboard on screen.
Then just use a layout as:
Column() {
LazyColumn(Modifier.weight(1f)) {
//..
}
Row(){
TextField()
Button()
}
}
This was exactly my problem with an extra twist that it only became an issue after the activity returned from pause.
A brand new state had no issue, but when resuming suddenly my entire mainactivity was being pushed up with the keyboard. SUPER weird behavior.
I actually set windowSoftInputMode to "adjustPan" abd it works as expected now, which is how it worked as a fresh activity.
I am making custom components and with my custom components I am required to cover the focus state.
At the top of my component hierarchy I am tracking focus with this.
val focused = remember { mutableStateOf(false) }
val focusModifier = modifier.onFocusEvent {
focused.value = it.hasFocus || it.isFocused
}
Component(modifier = focusModifier, focused = focused.value)
The component is basically this:
#Composable
fun Component(
modifier: Modifier = Modifier,
focused: Boolean = false
) {
...
val colorStuff = if(focused) focusColors else otherColors
var focusModifier = modifier
if(focused) {
focusModifier = modifier.border(BorderStroke(2.dp, Color.Red)).padding(16.dp)
}
NextComponent(
focusModifier,
colorStuff,
etc
)
}
If I leave the code with colorStuff and focused without the focusModifier code the focus state is done correctly and the colors for the component change appropriately. But when I add the focusModifier code and do a border and padding the focus state will trigger, but then instantly be lost. I'm assuming this is because the addition of modifier code changes the build order of the component and makes it discard focus. But that doesn't make full sense either.
I essentially need to add borders/shadows around components when they are focused so this will be something I have to do multiple times. Right now I can't get it done once. Any idea what I need to do to overcome this?
This is not an answer to your question, but I'll share my use-case, maybe it could help. Consider the codes below
data class PersonItem(
val id : Int,
val isActive: Boolean = false
)
List Item
#Composable
fun PersonInfoItem() {
Column {
// First Name
TextField(
modifier = Modifier.onFocusEvent {
// report to view model this personItem received focus events
},
value = "",
onValueChange = {
}
)
// Last Name
TextField(
modifier = Modifier.onFocusEvent {
// report to view model this personItem received focus events
},
value = "",
onValueChange = {
}
)
}
VieModel
class ViewModel {
val currentFocusedItem : PersonItem? = null
fun onPersonItemFocused(personItem: PersonItem) {
// this is where I validate that if the personItem argument
// is not the same with the currentFocusedPerson, if they are not then the last one is not on focus anymore (set !isActive)
// otherwise if they are the same, we are still on the same PersonItem
}
Regardless of which TextField received or lost its focus, as long as I'm keeping track of the Id of the PersonItem being reported to the ViewModel I'm guaranteed that if its the same PersonItem, it will retain what ever state I put into it during its Focus events
(say, the item will change background color when either of the TextField received focus, and revert back to its original background when none of the TextField is focused).
Another thing to consider, these kinds components (i.e TextFields) report multiple inactive focus events during its initial composition, and it's kind of annoying.
I'm implementing form controls, with validations etc. I want an error message to be shown only when a user "blurs" the TextField, in other words, when the field loses its focus. In Angular, we have touched state that we can proceed from. How to listen for losing focus state in Jetpack Compose?
#Composable
fun Screen() {
TextField(
onBlur = {
// P.S. This parameter does not exist
}
)
}
You can use onFocusChanged.
Sample Code:
var color by remember { mutableStateOf(Black) }
Box(
Modifier
.border(2.dp, color)
// The onFocusChanged should be added BEFORE the focusable that is being observed.
.onFocusChanged { color = if (it.isFocused) Green else Black }
.focusable()
)
Update Answer:
TextField API update - merged onFocus and onBlur callbacks into a single onFocusChange(Boolean) callback with parameter
Source: Version 0.1.0-dev15 - July 22, 2020
If anyone is still having this issue, I solved this using #snorlax answer, but improving on it. My problem was a TextField data validation. The TextField is by nature focusable, different from a Box, so adding .focusable() after defining the event was not doing what I planned: the validation happened once the component appeared on screen, and obviously we didn't want that.
On the .onFocusChanged modifier, we can check the event types and use it to change the blurred state, triggering then the validation.
I had a validate() function and a validState boolean state that I used between my presenter and my view, but I'll make it simple here and you may ignore the implementation of the state class:
We have the focus events, and we can emulate a blur event with a simple flag:
class FieldValidState {
var value: Boolean by mutableStateOf(true)
}
#Composable
fun TextFieldValidation(validState: FieldValidState = remember { FieldValidState() })
Column {
var isBlurred = false
TextField(
Modifier
.onFocusChanged {
if (!it.isFocused && isBlurred) validState.value = validate()
if (it.isFocused && !isBlurred) isBlurred = true }
)
}
}
So what happens here is: I want the validation function to be triggered after lost focus on the TextField. On startup, the field is inactive (i.e. it.isFocused == false) but I don't want to validate then. So once the focus is on the field and it was not blurred before, isBlurred becomes true and when it loses focus, it will be validated.
Let me know if this helps! Thanks!
I have create a simple example with six TextFields inside a LazyColumn, when you click the last TextField, the keyboard hides it, if you hide the keyboard and click again last TextField, works fine.
In the AndroidManifest I use "adjustPan"
android:windowSoftInputMode="adjustPan"
This is a capture when you click the last TextField first time, hides the last TextField
This is a capture when you click the last TextField second time, works correctly
This is the code
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestComposeTheme {
val numbers = listOf(1,2,3,4,5,6)
LazyColumn() {
items(numbers) { index->
TextField(index = index)
}
}
}
}
}
}
#Composable
fun TextField(index: Int){
var text by remember { mutableStateOf("Hello$index") }
TextField(
modifier = Modifier.padding(25.dp),
value = text,
onValueChange = { text = it },
label = { Text("TextField$index") }
)
}
Does anyone know if there is any way that the first time the last TextField is tapped, it would prevent the keyboard from hiding it
EDIT: There is a known issue:
192043120
This is already a known issue. https://issuetracker.google.com/issues/192043120
One hack to overcome this is use a column with verticalScroll
Column(Modifier.verticalScroll(rememberScrollState(), reverseScrolling = true){
// Content
}
put this in your app AndroidManifest.xml inside activity tag
<activity
...
android:windowSoftInputMode="adjustResize|stateVisible">
Suppose I have some activity with a jetpack-compose content
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ScrollableColumn(
modifier = Modifier
.fillMaxSize()
.border(4.dp, Color.Red)
) {
val (text, setText) = remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = setText,
label = {},
modifier = Modifier
.fillMaxWidth()
)
for (i in 0..100) {
Text("Item #$i")
}
}
}
}
}
If I were to launch this activity and focus on the TextField a software keyboard would pop up.
The interface, however, would not react to it. ScrollableColumn's bottom border (.border(4.dp, Color.Red)) would not be visible, as well as 100th item (Text("Item #$i")).
In other words, software keyboard overlaps content.
How can I make jetpack compose respect visible area changes (due to software keyboard)?
You can use the standard android procedure, but I don't know if a Compose specific way exists.
If you set the SoftInputMode to SOFT_INPUT_ADJUST_RESIZE, the Layout will resize on keyboard change.
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
setContent { /* Composable Content */ }
}
}
otherwise, you could use the flags in the manifest. See here for more information:
Move layouts up when soft keyboard is shown?
You can use Accompanist's inset library https://google.github.io/accompanist/insets
first use ProvideWindowInsets at the root of your composable hierarchy most of the time below your app theme compose and set windowInsetsAnimationsEnabled true
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
// content }
The use navigationBarsWithImePadding() modifier on TextField
OutlinedTextField(
// other params,
modifier = Modifier.navigationBarsWithImePadding() )
Finaly make sure to call WindowCompat.setDecorFitsSystemWindows(window, false) from your activity(inside onCreate). If you want Software keyboard on/off to animate set your activity's windowSoftInputMode adjustResize in AndroidManifests
<activity
android:name=".MyActivity"
android:windowSoftInputMode="adjustResize">
Besides Compose, no external libraries are needed for this now.
Set android:windowSoftInputMode=adjustResize on the activity in your manifest file
e.g.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".MyActivity"
android:windowSoftInputMode="adjustResize"/>
</application>
</manifest>
Then for the composable that you want the UI to react to, you can use Modifier.imePadding() which reacts to IME visibility
Box(Modifier.imePadding()) {
// content
}
I faced the same problem.
Use OnGlobalLayoutListener which will observe the actual IME rect size and will be triggered when the soft keyboard is fully visible.
Worked solution for me:
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scope = rememberCoroutineScope()
val view = LocalView.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
scope.launch { bringIntoViewRequester.bringIntoView() }
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
TextField(
modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester),
...
)
Origin here
I came up with idea of using accompanist insets library.
Someone could be interested because my approach does not modificate the contents of an application.
I link my post below:
(Compose UI) - Keyboard (IME) overlaps content of app
If you want your TextField scroll up when keyboard appears.
The following it work for me.
You need to add these
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
setContent { /* Composable Content */ }
}
And add this to your app/build.gradle
implementation "com.google.accompanist:accompanist-insets-ui:0.24.7-alpha"
In my case just adding android:windowSoftInputMode="adjustResize" to activity was enough to solve the problem.
It depends on how you build your UI. If yours screen's root is a vertically scrollable container or a Box, the keyboard resize might get managed automatically.
Use the modifier imePadding(). This will allow the specific compositions to adjust themselves when the keyboard pops up. This does not require you to set any flag on the activity