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)
}
)
}
Related
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
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
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.
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.
In Jetpack Compose there is a Modifier extension called selectable.
Configure component to be selectable, usually as a part of a mutually exclusive group, where
only one item can be selected at any point in time.
I'm using this for a mutually exclusive radio group inside a scrollable list. In my case a LazyColumn. This works fine, clicking on the selectable areas lights them up and results in detected clicks. However I noticed that the area also lights up while "touching" these areas while scrolling.
I made a simple example composable if you want to see what I mean, simply scroll through the list and you will see how scrolling triggers a short selected state:
#Composable
fun Example() {
LazyColumn {
item {
repeat(100){
Column(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.selectable(
selected = false,
onClick = { }
)
) {
Text("Example")
}
}
}
}
}
Has anyone figure out how to fix kind of behaviour? I tried looking for any related documentation at https://developer.android.com/jetpack/compose/gestures but nothing really explains how to "block" touch events while scrolling.
You can selectively enable Modifier.selectable(enabled) based on scroll state but even with derivedStateOf i see that there is huge performance loss.
val scrollState = rememberLazyListState()
val enableSelectable = derivedStateOf {
!scrollState.isScrollInProgress
}
Modifier
.fillMaxWidth()
.height(40.dp)
.selectable(
enabled = enableSelectable.value,
selected = false,
onClick = { }
)
I created a simple but longer example than you did, and included a video showing how it behaves with this code.
I believe what you are seeing is the ACTION_DOWN causing a ripple. It's not actually "selecting" the item because it does not change the selected state. I am not seeing the ripple when I scroll, but only when I keep my finger pressed on a specific row - the ripple disappears when my finger moves down.
I got the info about MotionEvents from this answer: https://stackoverflow.com/a/64594717/1703677
(Change the falses to true to see more info in the logs)
#Composable
fun Content() {
val selectedValue = remember { mutableStateOf("") }
LazyColumn {
item {
repeat(100) {
val label = "Item $it"
val selected = selectedValue.value == label
SingleRadioButtonWithLabel(label, selected) {
selectedValue.value = label
}
}
}
}
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SingleRadioButtonWithLabel(
label: String,
selected: Boolean,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.selectable(
selected = selected,
onClick = {
onClick()
Log.e("TestApp", "Row onClick")
}
)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
Log.e("TestApp", "MotionEvent.ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e("TestApp", "MotionEvent.ACTION_MOVE")
}
MotionEvent.ACTION_UP -> {
Log.e("TestApp", "MotionEvent.ACTION_UP")
}
else -> false
}
false
}
) {
RadioButton(
selected = selected,
onClick = {
onClick()
Log.e("TestApp", "Radio Button onClick")
},
)
Text(
text = label,
modifier = Modifier.fillMaxWidth()
)
}
}