TextField is overlapped by keyboard in Android Compose - android

I have a TextField in column with verticalScroll().
When adding a large number of characters, the textfield size goes beyond the keyboard and I stop seeing what I am typing
I tried to use this lib, but that's doesn't help

I think you can use BringIntoViewRequester in your TextField.
var state by rememberSaveable {
mutableStateOf("")
}
val coroutineScope = rememberCoroutineScope()
val bringIntoViewRequester = remember {
BringIntoViewRequester()
}
TextField(
value = state,
onValueChange = { text ->
state = text
// This will cause the TextField be repositioned on the screen
// while you're typing
coroutineScope.launch {
bringIntoViewRequester.bringIntoView()
}
},
modifier = Modifier
.bringIntoViewRequester(bringIntoViewRequester)
.onFocusChanged {
if (it.isFocused) {
coroutineScope.launch {
delay(400) // delay to way the keyboard shows up
bringIntoViewRequester.bringIntoView()
}
}
},
)
See the complete sample here.

you can add android:ellipsize="end" and android:maxLines="1" or whatever lines you want, in your text xml hope it would be helpful.

Related

How to prevent elements at the back from scrolling up when keyboard appears in Kotlin Compose? [duplicate]

This question already has answers here:
how to avoid jetpack compose content going up when keyboard opens
(2 answers)
Closed 25 days ago.
In this app, I want to keep the TextField at the Bottom while LazyColumn behind the TextField. But when I start typing, the keyboard appears which moves both TextField and the elements behind up. What I would prefer is if only the TextField moves up.
Before keyboard appears
After keyboard appears (the LazyColumn went up with the TextField)
#Composable
fun App() {
var todos = arrayOf("hello","hello","hello","hello","hello","hello","hello","hello")
var textFieldValue by remember { mutableStateOf("Hello World") }
MaterialTheme {
LazyColumn{
items(todos) {
Text(it)
}
}
Box(
modifier = Modifier.fillMaxHeight().fillMaxWidth(),
) {
TextField(
value = textFieldValue,
singleLine = true,
onValueChange = {
textFieldValue = it
},
modifier = Modifier.background(Color.White).fillMaxWidth().align(Alignment.BottomCenter),
trailingIcon = {
IconButton(
onClick = {
textFieldValue = ""
},
) {
Icon(
Icons.Rounded.Add,
contentDescription = "add a task",
)
}
},
)
}
}
}
You need to add android:windowSoftInputMode in the manifest of your activity
see https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input/visibility

Strange scroll in the BasicTextField with IntrinsicSize.Min

I need to implement UI control(decrease button, resizeable text input, increase button)
I was trying both ways - compose and XML version.
Compose version.
I have a strange scroll when I click on BasicTextField and pull horizontally. It just hides TextField value.
BasicTextField(
value = text,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
keyboardActions = remember { KeyboardActions(onDone = { keyboardController?.hide() }) },
onValueChange = { onTextChanged(it) },
singleLine = true,
decorationBox = { innerTextField ->
Box(modifier = Modifier.width(IntrinsicSize.Min)) {
innerTextField()
}
}
)
Modifier.width(1.dp, Dp.Infinity), doesn't help and takes empty horizontal padding, but it doesn't have the "internal" scroll.
1.1. How to fix the "internal" scroll in the BasicTextField with applied IntrinsicSize.Min?
1.2. Also BasicTextField cuts the left-most digit when you put cursor position. It would be great to have the same behavior as we have in the XML version.
XML version.
I was trying to use the XML version with EditText instead of Compose BasicTextField but stuck with merge compose state and EditText TextWatcher listener.
2.1. To avoid twice setting the same text value to the EditText, I should compare compose state value and EditText value each time in the update {} block, is that ok? Or here can be a better solution?
AndroidView(
factory = {
_binding = L7Binding.inflate(inflater, container, false)
val view = _binding!!.root
_binding!!.editText.doOnTextChanged { text, _, _, _ ->
viewModel.onTextChanged(text.toString())
}
view
},
update = {
if (_binding!!.editText.text.toString() != inputText) {
_binding!!.editText.setText(inputText)
}
}
)
I faced the same problem.
Solved it with the help of this code, I hope it will be useful to you)
BasicTextField(,
modifier = Modifier
.disabledHorizontalPointerInputScroll()...
and also
private val HorizontalScrollConsumer = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource) = available.copy(y = 0f)
override suspend fun onPreFling(available: Velocity) = available.copy(y = 0f)
}
fun Modifier.disabledHorizontalPointerInputScroll(disabled: Boolean = true) =
if (disabled) this.nestedScroll(HorizontalScrollConsumer) else this

Jetpack Compose animation finished listener not called

I'm struggling to use Jetpack Compose Animation to achieve a (supposedly simple) effect:
in the case of an error, a control's background color should flash red, and after a short delay then fade back to normal (transparent).
My current approach is to model this with a boolean state shouldFlash which is set to true when an error occurs, and is set back to false when the animation completes. Unfortunately it seems the finishedListener passed to animateColorAsState is never called. I attached a debugger and also added a log statement to verify this.
What am I doing wrong?
Sample (button triggers error):
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
FlashingText(flashing = shouldFlash, text = text) {
shouldFlash = false
text = "Animation done"
}
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
#Composable
fun FlashingText(flashing: Boolean, text: String, flashFinished: () -> Unit) {
if (flashing) {
val flashColor by animateColorAsState(
targetValue = Color.Red,
finishedListener = { _ -> flashFinished() }
)
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
} else {
Text(text = text)
}
}
Compose version: 1.0.5 (latest stable at time of writing), also in 1.1.0-beta02
Anyway, to understand this, you need to know how the animateColorAsState works internally.
It relies on recompositions - is the core idea.
Every time a color changes, a recomposition is triggered, which results in the updated color value being reflected on the screen. Now, what you are doing is just using conditional statements to DISPLAY DIFFERENT COMPOSABLES. Now, one Composable is actually referring to the animating value, that is, the one inside your if block (when flashing is true). On the other hand, the else block Composable is just a regular text which does not reference it. That is why you need to remove the conditional. Anyway, because after removing the conditional, what remains is only a single text, I thought it would be a waste to create a whole new Composable out of it, which is why I removed that Composable altogether and pasted the Text inside your main Composable. It helps to keep things simpler enough. Other than this, the answer by #Rafiul does work, but there is not really a need for a Composable like that, so I would still recommend using this answer instead, so that the code is easier to read.
ORIGINAL ANSWER:
Try moving the animator outside the Child Composable
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
val flashFinished: (Color) -> Unit = {
shouldFlash = false
text = "Animation done"
}
val flashColor by animateColorAsState(
targetValue = if (shouldFlash) Color.Red else Color.White,
finishedListener = flashFinished
)
//FlashingText(flashing = shouldFlash, text = text) -> You don't need this
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
Change your code like this.
FlashingBackground
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
FlashingText(flashing = shouldFlash, text = text) {
shouldFlash = false
text = "Animation done"
}
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
FlashingText
#Composable
fun FlashingText(flashing: Boolean, text: String, flashFinished: () -> Unit) {
val flashColor by animateColorAsState(
targetValue = if(flashing) Color.Red else Color.White,
finishedListener = { _ -> flashFinished() }
)
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
}
Edited:
The problem with your code is you are initializing animateColorAsState when you are clicking the Flash button and making shouldFlash = true. So for the first time, it just initializes the animateColorAsState, doesn't run the animation. So there will be no finishedListener call as well. Since finishedListener isn't executed, shouldFlash stays to true. So from the next call shouldFlash is already true there will be no state change. That's why from the subsequent button click, it doesn't recompose the FlashingText anymore. You can put some log in your method you won't see FlashingText after the first button click.
Keep in mind: targetValue = Color.Red will not do anything. target value should be either a state or like this condition if(flashing) Color.Red because you need to change the state to start the animation.
#Phillip's answer is also right. But I don't see any extra advantage in moving the animator outside the Child Composable if you use it like the above.

How can I make my Composable Recompose with a For Loop Update?

I understand that architecturally this is definitely not a good thing to do, but I have embedded a for loop in a composable to update state as follows:
#Composable
fun WorkScreen(name: String?) {
var text by remember {
mutableStateOf(0)
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(text = "YOU PRESSED ME $text")
}
for (i in 1..100) {
text = i
}
}
My expectation is that when I switch to this screen the for loop should update the mutableState and hence cause a recomposition which causes the time to tick up. However, instead I just get YOU PRESSED ME 0 if I put the for loop below the Box function, or I get YOU PRESSED ME 100 if I put it above the Box function.
The following question: Why my composable not recomposing on changing value for MutableState of HashMap?, does seem to be quite similar, but I'm not sure how it applies here. It seems to me I am updating the text value to be i!
You shouldn't change view state directly from the composable view builder, because compose functions will be recalled often during recomposition, so your calculation will be repeated. You should use side effects instead.
If you need to show dynamic change of the value to user, then you should use animation, as Gabriele's answer suggests.
An other option is updating the value manually. Inside LaunchedEffect you can use suspend functions, so you can change the value with a needed delay:
LaunchedEffect(Unit) {
for (i in 1..100) {
delay(1000) // update once a second
text = i
}
}
You should use an animation where you define how often you want to update the text applying it with a side effect.
For example:
var targetValue by remember { mutableStateOf(0) }
val value by animateIntAsState(
targetValue = targetValue,
animationSpec = tween( durationMillis = 2000 )
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(text = "YOU PRESSED ME $value")
}
LaunchedEffect(Unit) {
targetValue = 100
}

Prevent the keyboard from appearing in a Jetpack Compose app

I'm making a calculator to learn Compose, so I placed my own number buttons on screen and I wanted to prevent the soft keyboard from appearing.
Here is my repo: https://github.com/vitor-ramos/CalculadorCompose
I noticed in TextFieldImpl.kt there is a modifier to show the keyboard, so I tried to clone the code and remove the line: keyboardController.value?.showSoftwareKeyboard() I know it's not a good idea to duplicate code like that, but I wanted to give it a try, and it didn't work. As you can see in the original code below there's a TODO saying it should be handled by BaseTextField, but I looked in it's code and didn't find where it shows or hides the keyboard.
val textFieldModifier = modifier
.focusRequester(focusRequester)
.focusObserver { isFocused = it.isFocused }
.clickable(indication = null) {
focusRequester.requestFocus()
// TODO(b/163109449): Showing and hiding keyboard should be handled by BaseTextField.
// The requestFocus() call here should be enough to trigger the software keyboard.
// Investiate why this is needed here. If it is really needed, instead of doing
// this in the onClick callback, we should move this logic to the focusObserver
// so that it can show or hide the keyboard based on the focus state.
keyboardController.value?.showSoftwareKeyboard()
}
I found in this question that with views I can extend EditText and change the functionality, but I haven't found a equivalent for Compose: Android: Disable soft keyboard at all EditTexts
public class NoImeEditText extends EditText {
public NoImeEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onCheckIsTextEditor() {
return false;
}
}
I have tested Arun Pdiyan solution and works like a charm with null LocalTextInputService (in my case I read data from device attached Barcode reader)
CompositionLocalProvider(
LocalTextInputService provides null
) {
TextField(
value = barcodeReaderService.readedText.value,
onValueChange = { textState.value=it },
label = { Text("The Label") }
)
}
Explanation
I created a Composable ReadonlyTextField, that places a invisible box in front of the text field. The box has the same size as the text field.
With that workaround you can't focus the text field anymore, so no keyboard appears. In order to apply custom click handling, i added a onClick to the Box-Modifier.
This is not really a clean solution, but a good workaround.
Implementation of ReadonlyTextField
#Composable
fun ReadonlyTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
onClick: () -> Unit,
label: #Composable () -> Unit
) {
Box {
TextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = label
)
Box(
modifier = Modifier
.matchParentSize()
.alpha(0f)
.clickable(onClick = onClick),
)
}
}
Usage of ReadonlyTextField
#Composable
fun App() {
val textState = remember { mutableStateOf(TextFieldValue()) }
Column {
ReadonlyTextField(
value = textState.value,
onValueChange = { textState.value = it },
onClick = {
// custom click handling (e.g. open dialog)
},
label = {
Text(text = "Keyboardless Input")
}
)
}
}
A complete integrated example can be found in my medium post:
https://caelis.medium.com/jetpack-compose-datepicker-textfield-39808e42646a
Credits also go to this stackoverflow answer:
Jetpack Compose: Disable Interaction with TextField
You can hide keyboard on compose by providing TextInputService to TextField. You can implement your TextInputService or just pass it null for disabling input service.
CompositionLocalProvider(
// You can also provides null to completely disable the default input service.
LocalTextInputService provides myTextInputService
) {
BaseTextField(...)
}
You may see google employee answer here about this subject.
With ReadonlyTextField it is not possible to position the cursor. So added wrapped EditText inside a compose AndroidView
#Composable
fun NoKeyboardTextField(
modifier: Modifier,
text: String,
textColor: Int
) {
AndroidView(
modifier = modifier,
factory = { context ->
AppCompatEditText(context).apply {
isFocusable = true
isFocusableInTouchMode = true
showSoftInputOnFocus = false
}
},
update = { view ->
view.setText(text)
view.setTextColor(textColor)
view.setSelection(text.length)
}
)
}

Categories

Resources