Search code examples
androidkotlinandroid-jetpack-composetextfieldtextwatcher

Undo/Redo, how to handle doOnTextChanged and doBeforeTextChanged in TextField of Jetpack Compose?


I need to implement redo/undo for TextField in Jetpack Compose. For an EditText I used this one and it worked well. However, for Jetpack Compose there is no such a listener. I would implement an own one based on that one for EditText, but I'm missing these two listener methods, which are not available for TextField:

doOnTextChanged { text, start, before, count -> }
doBeforeTextChanged { text, start, count, after -> }

In TextField is only one listener to use

onValuesChange = { }

that only string without start and count returns.

How would I achieve a redo/undo to implement for a TextField in Jetpack Compose?

Edit

This is what I did so far. Would be great to make it functionable:

class EditTextDo {

    private var mIsUndoOrRedo = false
    private val editHistory: EditHistory? = null

    fun redo() {
        val edit = editHistory?.getNext() ?: return

        // Do Redo
    }

    fun undo() {
        val edit = editHistory?.getPrevious() ?: return

        // Do Undo
    }

    fun canUndo(): Boolean {
        editHistory?.let {
            return it.position > 0
        }
        return false
    }

    fun canRedo(): Boolean {
        editHistory?.let {
            return it.position < it.history.size
        }
        return false
    }

}

class EditHistory {

    var position = 0

    private var maxHistorySize = -1

    val history = LinkedList<EditItem>()

    private fun clear() {
        position = 0
        history.clear()
    }

    fun add(item: EditItem) {
        while (history.size > position) {
            history.removeLast()
        }
        history.add(item)
        position++
        if (maxHistorySize >= 0)
            trimHistory()
    }

    fun getNext(): EditItem? {
        if (position >= history.size) {
            return null
        }
        val item = history[position]
        position++
        return item
    }

    fun getPrevious(): EditItem? {
        if (position == 0) {
            return null
        }
        position--
        return history[position]
    }

    private fun setMaxHistorySize(maxHistorySize: Int) {
        this.maxHistorySize = maxHistorySize
        if (maxHistorySize >= 0)
            trimHistory()
    }

    private fun trimHistory() {
        while (history.size > maxHistorySize) {
            history.removeFirst()
            position--
        }
        if (position < 0)
            position = 0
    }
}

data class EditItem(val start: Int, val before: CharSequence, val after: CharSequence)

Solution

  • This may not exactly address your post but hopefully it help. I have a simple undo/redo Textfield on my project using Queue structure to keep track of the input history, I'm not specifying history size though.

    @Composable
    fun TextFieldWithHistory() {
    
        val undoRedoState = remember { UndoRedoState() }
    
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight()
        ) {
            TextField(
                value = undoRedoState.input,
                onValueChange = {
                    undoRedoState.onInput(it)
                }
            )
    
            Button(
                onClick = {
                    undoRedoState.undo()
                }) {
                Text(text = "Undo")
            }
    
    
            Button(
                onClick = {
                    undoRedoState.redo()
                }) {
                Text(text = "Redo")
            }
        }
    }
    
    
    class UndoRedoState {
    
        var input by mutableStateOf(TextFieldValue(""))
        var undoHistory = ArrayDeque<TextFieldValue?>()
        var redoHistory = ArrayDeque<TextFieldValue?>()
    
        init {
            undoHistory.add(input)
        }
    
        fun onInput(value: TextFieldValue) {
            // always set the cursor at the end (selection = text length)
            val updatedValue =  value.copy(value.text, selection = TextRange(value.text.length))
            undoHistory.add(updatedValue)
            input = updatedValue
        }
    
        fun undo() {
    
            if (undoHistory.size > 1) {
    
                // pop the last
                val pop = undoHistory.removeLastOrNull()
                pop?.let {
                    if (it.text.isNotEmpty()) {
                        redoHistory.add(it)
                    }
                }
    
                // peek the last
                val peek = undoHistory.lastOrNull()
                peek?.let{
                    input = it
                }
            }
        }
    
        fun redo() {
            val pop = redoHistory.removeLastOrNull()
            pop?.let {
                if (it.text.isNotEmpty()) {
                    undoHistory.add(it)
                    input = it
                }
            }
        }
    }