How to detect TextField cursor lies on which line in Android Jetpack compose.
Asked
Active
Viewed 233 times
2 Answers
4
To have access to selected text position, you need TextFieldValue
.
If you have short lines and you are sure that they will not break in any way other than \n
symbol, you can manually calculate the current line.
For a more general solution, which does not depend on the format of your data, you can use the TextLayoutResult
value to get this information.
onTextLayout
is only available with BasicTextField
, but using a decorationBox
you can easily turn it into a TextField
/OutlinedTextField
.
var textFieldValue by remember {
mutableStateOf(TextFieldValue(LoremIpsum().values.first()))
}
var textLayoutResult by remember {
mutableStateOf<TextLayoutResult?>(null)
}
val cursorLine by remember {
derivedStateOf {
textLayoutResult?.getLineForOffset(textFieldValue.selection.start)
}
}
val interactionSource = remember { MutableInteractionSource() }
Column {
Text("Line: ${cursorLine.toString()}")
BasicTextField(
value = textFieldValue,
onValueChange = { textFieldValue = it },
interactionSource = interactionSource,
onTextLayout = {
textLayoutResult = it
},
decorationBox = { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = textFieldValue.text,
innerTextField = innerTextField,
enabled = true,
singleLine = false,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
)
}
)
}

Phil Dukhov
- 67,741
- 15
- 184
- 220
2
You can get indexes of new line characters '\n' or others and then loop through each of them to get line count.
Result
fun getLine(textFieldValue: TextFieldValue): Int {
val text = textFieldValue.text
val selection = textFieldValue.selection.start
val lineList = mutableListOf< Int>()
text.forEachIndexed { index: Int, c: Char ->
if (c == '\n') {
lineList.add(index)
}
}
if (lineList.isEmpty()) return 1
lineList.forEachIndexed { index, lineEndIndex ->
if (selection <= lineEndIndex){
return index + 1
}
}
return lineList.size + 1
}
If you wish to get not only line but row too you can do it by using a map
fun getLineAndRowOfSelection(textFieldValue: TextFieldValue): Pair<Int, Int> {
val text = textFieldValue.text
val selection = textFieldValue.selection.start
val lineMap = linkedMapOf<Int, Int>()
var lineCount = 1
text.forEachIndexed { index: Int, c: Char ->
if (c == '\n') {
lineMap[index] = lineCount
lineCount++
}
}
if (lineMap.isEmpty()) {
return Pair(1, selection)
} else if (lineMap.keys.last() < selection) {
// Selection is in a row after last new line char
val key = lineMap.keys.last()
val lastLine = lineMap[key] ?: 0
return Pair(lastLine + 1, selection - key - 1)
} else {
// Selection is before last new line char
var previousLineIndex = -1
lineMap.forEach { (lineEndIndex, line) ->
if (selection <= lineEndIndex) {
// First line
return if (previousLineIndex == -1) {
Pair(line, selection)
} else {
Pair(line, selection - previousLineIndex - 1)
}
}
previousLineIndex = lineEndIndex
}
}
return Pair(-1, -1)
}
Demo
@Preview
@Composable
private fun TextSelectionTest() {
Column(
modifier = Modifier.padding(top = 30.dp)
) {
var textFieldValue by remember {
mutableStateOf(TextFieldValue())
}
val lineAndRow = getLineAndRowOfSelection(textFieldValue)
val line = getLine(textFieldValue)
Text(
"Text ${textFieldValue.text}, " +
"selection: ${textFieldValue.selection}\n" +
"line and row: $lineAndRow\n" +
"line: $line"
)
TextField(value = textFieldValue, onValueChange = { textFieldValue = it })
}
}

Thracian
- 43,021
- 16
- 133
- 222