Vivek Modi
Vivek Modi

Reputation: 7291

Cursor doesn't move after filtering text Jetpack Compose

I'm using InputTransformation in Jetpack Compose to filter user input in a BasicTextField. The transformation works correctly, but the cursor remains in the same position after entering text instead of moving backward.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.maxLength
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.then
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            val stateOne = rememberTextFieldState(initialText = "Hellow")
            val stateTwo = rememberTextFieldState()
            BasicTextFieldExamples(
                stateTwo,
                remember { MutableInteractionSource() },
            )
        }
    }

    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun BasicTextFieldExamples(
        stateOTwo: TextFieldState,
        secondInteractionSource: MutableInteractionSource,
    ) {
        Column(
            Modifier
                .fillMaxSize()
                .padding(50.dp)
        ) {
            BasicTextField(
                state = stateOTwo,
                modifier = Modifier
                    .padding(top = 100.dp)
                    .fillMaxWidth()
                    .height(56.dp),
                inputTransformation = InputTransformation.maxLength(6)
                    .then(LetterOnlyTransformation()),
                interactionSource = secondInteractionSource,
                decorator = TextFieldDefaults.decorator(
                    state = stateOTwo,
                    enabled = true,
                    label = {
                        Text("Last Name")
                    },
                    placeholder = {
                        Text("Example 2")
                    },
                    lineLimits = TextFieldLineLimits.Default,
                    interactionSource = secondInteractionSource,
                    outputTransformation = null
                )
            )
        }
    }
}

class LetterOnlyTransformation : InputTransformation {
    override val keyboardOptions: KeyboardOptions?
        get() = KeyboardOptions(keyboardType = KeyboardType.Text)

    override fun TextFieldBuffer.transformInput() {
        val filteredValue = asCharSequence().filter { it.isLetter() }
        if (filteredValue != asCharSequence()) {
            replace(0, length, filteredValue)
        }
    }
}

Question:

How can I ensure that the cursor moves correctly after filtering input in InputTransformation? Is there a better way to handle this scenario in Jetpack Compose?

Any help is appreciated!

See the problem in here

Upvotes: 2

Views: 86

Answers (1)

Vlad Guriev
Vlad Guriev

Reputation: 1934

This issue occurs because TextFieldBuffer.transformInput() is called even during selection or cursor position changes, and the condition if (filteredValue != asCharSequence()) always returns true (this comparison would work as expected if they were String objects where the equals method is overridden).

As a result, replace(0, length, filteredValue) is called every time you move the cursor, and this method reverts cursor position changes.

You can use revertAllChanges() instead – it handles this under the hood.

Additionally, you can track the changes.changeCount value to return from transformInput() when changeCount is 0 (which occurs when the input text itself is not changing – e.g., during cursor position changes).

Here's how the transformInput() method may look:

@OptIn(ExperimentalFoundationApi::class)
override fun TextFieldBuffer.transformInput() {
    if (changes.changeCount == 0) return
    val currentInput = asCharSequence().toString()
    val filteredValue = currentInput.filter { it.isLetter() }
    if (filteredValue != currentInput) {
        revertAllChanges()
    }
}

Upvotes: 0

Related Questions