Status Update
Comments
ae...@google.com <ae...@google.com>
zh...@gmail.com <zh...@gmail.com> #2
```
var textFieldValueState by remember {
mutableStateOf(
TextFieldValue(
text = value, selection = when {
value.isEmpty() -> TextRange.Zero
else -> TextRange(value.length, value.length)
}
)
)
}
```
However, as this is set up as internals of BasicTextField, in each case of using BasicTextField, TextField, and OutlinedTextField (and any other text field) this code needs to re-duplicate the full API surface.
It would be nice if there was a parameter to control initial position of the cursor even if a String is provided rather than default to 0.
mu...@gmail.com <mu...@gmail.com> #3
#2 Your workaround doesn't work, because for some reason the TextLayoutResultProxy.decorationBoxCoordinates
ends up as null
in the Offset.coercedInVisibleBoundsOfInputText()
on the first touch changing the TextRange(value.length, value.length)
that you are setting to TextRange(0, 0)
ea...@gmail.com <ea...@gmail.com> #4
di...@gmail.com <di...@gmail.com> #5
I understand it's P4 but still :/
Sucks to be a P4
er...@gmail.com <er...@gmail.com> #6
As #2 commented, solving the issue seems pretty easy, even a bit simpler that his/her example:
var textFieldValueState by remember {
mutableStateOf(
TextFieldValue(
text = value,
selection = TextRange(value.length),
)
)
}
In the original overloaded implementation of the BasicTextField
that has the value
parameter of String
type, the default value of the selection
parameter for the textFieldValueState
field was not being set explicitly, resulting on taking the default value, which is TextRange.Zero
, and that means the cursor has to be set at the beginning of the text.
So, overloading this Composable and just adding selection = TextRange(value.length)
there, the issue seems to be solved.
@Composable
fun MyBasicTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = TextStyle.Default,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
visualTransformation: VisualTransformation = VisualTransformation.None,
onTextLayout: (TextLayoutResult) -> Unit = {},
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
cursorBrush: Brush = SolidColor(Color.Black),
decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
@Composable { innerTextField -> innerTextField() }
) {
// Holds the latest internal TextFieldValue state. We need to keep it to have the correct value
// of the composition.
var textFieldValueState by remember {
mutableStateOf(
TextFieldValue(
text = value,
selection = TextRange(value.length),
)
)
}
// Holds the latest TextFieldValue that BasicTextField was recomposed with. We couldn't simply
// pass `TextFieldValue(text = value)` to the CoreTextField because we need to preserve the
// composition.
val textFieldValue = textFieldValueState.copy(text = value)
SideEffect {
if (textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition
) {
textFieldValueState = textFieldValue
}
}
// Last String value that either text field was recomposed with or updated in the onValueChange
// callback. We keep track of it to prevent calling onValueChange(String) for same String when
// CoreTextField's onValueChange is called multiple times without recomposition in between.
var lastTextValue by remember(value) { mutableStateOf(value) }
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
if (stringChangedSinceLastInvocation) {
onValueChange(newTextFieldValueState.text)
}
},
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
visualTransformation = visualTransformation,
onTextLayout = onTextLayout,
interactionSource = interactionSource,
cursorBrush = cursorBrush,
decorationBox = decorationBox,
)
}
Hope this helps everyone to easily workaround the issue, and to Google for adding this fix asap.
da...@ahiho.com <da...@ahiho.com> #8
#6 but if we use selection = TextRange(value.length)
, the text field now can't move the cursor, the cursor will be fixed to the last. If user want to move the cursor to the first to edit some, they just can't do that
lo...@gmail.com <lo...@gmail.com> #9
al...@rockettrade.com <al...@rockettrade.com> #10
Looking at possible solutions offered in this issue, in my scenario, I still need to be able to click anywhere in the textfield and the cursor to show up the closest to where I clicked for quicker edits. I don't know if that would work. Right now I have to just live with the fact that it default to the beginning in order to not lose on the aforementioned functionality.
Description
Jetpack Compose version: 1.3.1 (BOM 2023.01.00)
Jetpack Compose component used: Foundation, UI, Material
Android Studio Build: Electric Eel 2022.1.1
Kotlin version: 1.8.10
Steps to Reproduce or Code Sample to Reproduce:
When using the
androidx.compose.foundation.text.BasicTextField
, which accepts astring
by default instead of aTextFieldValue
, the component during initialization does not set the cursor selection to the end of the provided string. This can be problematic when using the software keyboard to move focus to the next field using IME Actions, since the cursor position will default to the start.I provided an example using the
OutlinedTextField
. It seems incorrect that the default behavior does not initialize the cursor selection to the end of the initially provided value.See attached video for demo of issue.