2

How to detect TextField cursor lies on which line in Android Jetpack compose.

enter image description here

Tippu Fisal Sheriff
  • 2,177
  • 11
  • 19

2 Answers2

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

enter image description here

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