百度去搜的话,极有可能会搜到和栈相关的东西,但是其实我们可以想的简单点,撤销的地方就是存一下上一步的文本内容,反撤销就是存着撤销中的内容,所以我们其实用两个类似列表的东西就能实现这个功能。
因此,以下的功能都是基于ArrayDeque来实现的,翻译一下Deque,它的中文名叫双队列。
不是很明白它的使用形式,但是应该是和列表差不多的。
思路很简单
撤销:填充上一步内容
反撤销:填充被撤销的内容
所以基于以上思路我们先创建变量:
class TextControl {
var undoHistory = ArrayDeque()
var redoHistory = ArrayDeque()
}
fun undo(
contentText: MutableState,
unDoState: MutableState,
reDoState: MutableState,
) {
if (undoHistory.size > 1) {
// pop the last
reDoState.value = true
redoHistory.add(undoHistory.removeLast())
// peek the last
contentText.value = undoHistory.last()
remValTextBefore =
TextFieldValue(contentText.value.text, TextRange(contentText.value.text.length))
// 到这时,已经完成弹出,这时判断一下就能实现自己禁用自己
if (undoHistory.size == 1) {
unDoState.value = false
}
}
}
既然是对内容操作,肯定会用到全局的内容变量contentText,而后面两个状态参数呢,是用来控制按钮的使能的,就是在撤销列表或者反撤销列表为空时让它不能点击(disable)
先看判断,判断它的size大于1,因为我们在初始化的时候要将内容放到撤销列表中,不然我们第一次操作将无法撤销。
当然,这个函数执行时,说明我们点击了撤销按钮,因此就要将列表的最后一个元素添加到反撤销列表中,并将撤销列表的最后一个元素赋值给内容变量。
这里还给remValTextBefore 进行了赋值,是我在后来调试的时候发现有点小bug,主要表现在如果不加这句的话,点击撤销一次后,反撤销会亮起,但是如果此时切换了光标,那么反撤销就会变暗,显然变化光标并没有对文本进行改动,所以不应该清空反撤销列表。
fun redo(
contentText: MutableState,
unDoState: MutableState,
reDoState: MutableState
) {
if (redoHistory.size > 0) {
val pop = redoHistory.removeLast()
undoHistory.add(pop)
unDoState.value = true
contentText.value = pop
remValTextBefore =
TextFieldValue(contentText.value.text, TextRange(contentText.value.text.length))
if (redoHistory.size == 0) {
reDoState.value = false
}
}
}
和撤销有区别的是,反撤销列表一开始不用存放任何内容,所以它的size一开始应该是0。
当我们点击反撤销时,就得将它的最后一个元素拿到,一边存入撤销列表,一边给内容变量。(注意和撤销函数的区别)
状态变量我就不说了,就是相互使能。
在有了撤销和反撤销函数后,还要在文本变化过程中的正确时刻添加undoHistory,所以这里就会和上一篇讲的onInput函数有交集
首先是onInput函数的修改:
fun onInput(
value: TextFieldValue,
contentText: MutableState,
unDoState: MutableState,
reDoState: MutableState,
)
接着要在初始化时保存文本内容到撤销列表:
if (remValTextBefore == null) {
remValTextBefore = value
undoHistory.add(value)
} else {
if (remValTextAfter == null) {
remValTextAfter = value
}
}
之后就是在我们输入换行检测段代码的基础上添加了:
1、防止列表太大,所以设定了15的长度,就是最多连续点击撤销14次,因为它原本里面还放了一次。
2、文本发生变化时就要保存到撤销列表中
3、对两个按钮的状态进行实时判断,防止它在不该使能的时候使能。
这样这个功能就实现了,接下来要添加到控件中。
首先是声明
val textControl = remember { TextControl() }
val unDoState = remember { mutableStateOf(false) }
val reDoState = remember { mutableStateOf(false) }
还是初始化和输入框处
接着是按钮
IconButton(
onClick = {
textControl.undo(contentText, unDoState, reDoState)
},
//通知设置layoutId实现控制约束
modifier = Modifier
.height(50.dp)
.width(40.dp)
.layoutId("btn_undo"),
enabled = unDoState.value,
) {
Icon(
painter = painterResource(id = R.drawable.btn_undo),
contentDescription = "add a book",
)
}
IconButton(
onClick = {
textControl.redo(contentText, unDoState, reDoState)
},
//通知设置layoutId实现控制约束
modifier = Modifier
.height(50.dp)
.width(40.dp)
.layoutId("btn_redo"),
enabled = reDoState.value
) {
Icon(
painter = painterResource(id = R.drawable.btn_redo),
contentDescription = "add a book"
)
}
注意它们的enabled要设置成我们声明的状态。
这下基本就完成了!
还要在切换章节时清空两个列表,比如drawer中:
剩下的底部工具栏的切换按钮也需要添加,这里就不展示了。
这下就完事了!看看效果: