写小说的时候分行其实挺重要的,一大段文字挨在一块属实看不下去,别说读者了就是自己看起来都很费力,所以每次插入回车时多插入一行是很有必要的。这次时偏向于算法的实现,有点烧脑可能。
先看效果:
当然我写的这个程序有个问题,就是在最开始的地方插入换行的时候很不稳定:
所以这个问题更适合在初始化的时候解决,先一步步来看!
还是老样子,梳理思路,
实现这个功能首先就是插入一个回车,这时候要拦截这个回车做一些修改,那么从哪里获取换行符呢?
没错,是在TextField的onValueChange函数中。
onValueChange会在文本发生变化时调用,这里的文本变化不单单是指增加或者删除文本,还包括光标位置变化,英文单词拼写。
所以第一,我们做换行符拦截就要过滤其他方式的文本变化。
其次,每次插入换行符进行拦截,那我们怎么知道它插入了换行符?这里就要保存上一步的文本信息,然后拿来对比。所以我们还需要保存修改前的文本。
先有这两步之后,我们就能开始写代码了:
首先声明两个变量,用来存放变化前后的文本。
private var remValTextBefore: TextFieldValue? = null
private var remValTextAfter: TextFieldValue? = null
注意,这里的类型是TextFieldValue,这个类型会包含光标信息,看它代码:
class TextFieldValue constructor(
val annotatedString: AnnotatedString,
selection: TextRange = TextRange.Zero,
composition: TextRange? = null
)
主要由三部分组成,文本,光标状态,拼写。
文本用.text来获取
光标状态则分为start和end两种
拼写是在英文拼写单词时触发的一种特别状态,这里不解释了,我也没用过而且如果加上它的话会大幅度增加文本的处理难度。
具体在后面用的时候就知道了!
我们将其分成两块,一块是外层的逻辑,另一块是对文本的处理。
fun onInput(
value: TextFieldValue,
contentText: MutableState,
) {
// 发现textfield在获得焦点和失去焦点时都会调用onValueChange这个函数,也就是会调用本函数,而且在首次获得焦点时会调用两次
//所以迫不得已只能创建两个变量来监控文本的变化了
// 12.30 发现只要是撤销或者反撤销都会导致onChange函数触发,所以在函数的最外层不能直接写任何语句
if (remValTextBefore == null) {
remValTextBefore = value
} else {
if (remValTextAfter == null) {
remValTextAfter = value
}
}
}
首先是变化前后的文本获取,我们可以通过上面这种代码的形式,将初始化时的文本保存起来。
那么这个onInput函数就得在初始化时调用一次才行。(初始化一路看过来的应该知道,LaunchEffect中嘛,这里就不说了,就加一行代码的事)
好有了这个之后呢,我们就可以开始过滤文本信息了:
if (remValTextAfter != null) {
remValTextAfter = value
// 1、文本发生变化触发
if (remValTextBefore!!.text != remValTextAfter!!.text) {
var realText = value
// 2、文本长度变长才会触发,这种情况下才有可能是插入了换行
if (remValTextAfter!!.text.length > remValTextBefore!!.text.length) {
// 3、变化后的文本起始和终止位置相同,说明不是选了一段文本或是粘贴了一段文本
// 这时有可能是插入换行,也可能是插入了新内容
if (remValTextAfter!!.selection.start == remValTextAfter!!.selection.end) {
}
contentText.value = realText
} else {
contentText.value = realText
}
// 只适用于中文以及不需要拼写的语言
remValTextBefore =
TextFieldValue(remValTextAfter!!.text, TextRange(remValTextAfter!!.text.length))
} else {
contentText.value = value
}
else {
contentText.value = value
}
首先就是文本要变化嘛,就是remValTextAfter不为空,
接着是前后文本变化不一致才触发,不然换个光标位置也会触发这个函数
再下来是后面的文本比前面的长,插入换行符肯定是文本增加嘛,不可能越插越短
再往下是个
remValTextAfter!!.selection.start == remValTextAfter!!.selection.end
这是啥?
selection这个参数中,包含了光标的起始位置start和结束位置end,如果你在某个文字处点击光标,那他的位置是一样的,如果你选中了一段文字,那它的两个位置就不一样了。
所以我们这要排除选中的情况。
这个时候,就真正到了我们想要处理的那种文本处了。
还要注意基本每个地方都有else,其中是contentText.value = value,也就是说,如果不符合我们要处理的情况,那就要直接给全局变量赋值,这样才能保证文本框的值一直在更新。
if (remValTextAfter!!.selection.start == remValTextAfter!!.selection.end) {
// 4、判断插入换行的位置:
// a、文本开头,b、文本中间,c、文本末尾
// 如何判断:根据文本变化前后的selection的start或end,此时应该都是一样的
val enterSymbolChar = remValTextAfter!!.text.substring(
remValTextAfter!!.selection.start - 1,
remValTextAfter!!.selection.start
).toCharArray()
if (enterSymbolChar.size == 1) {
if (enterSymbolChar[0] == '\n') {
// c:
if (remValTextAfter!!.selection.start > remValTextBefore!!.selection.start) {
val frontText = remValTextAfter!!.text.substring(0, remValTextAfter!!.selection.start - 1)
val newText = StringBuilder()
newText.append(frontText)
newText.append('\n')
newText.append('\n')
newText.append(" ")
val newTextFieldValue =
value.copy(newText.toString(),TextRange(newText.length))
realText = newTextFieldValue
remValTextAfter = newTextFieldValue
}
// b:
if (remValTextAfter!!.selection.start <=
remValTextBefore!!.selection.start && remValTextAfter!!.selection.start > 1) {
val frontText = remValTextAfter!!.text.substring(0, remValTextAfter!!.selection.start - 1)
val backText = remValTextAfter!!.text.substring(remValTextAfter!!.selection.start,
remValTextAfter!!.text.length)
val newText = StringBuilder()
newText.append(frontText)
newText.append('\n')
newText.append('\n')
newText.append(" ")
val newTextLength = newText.length
newText.append(backText)
val newTextFieldValue =value.copy(newText.toString(), TextRange(newTextLength))
realText = newTextFieldValue
remValTextAfter = newTextFieldValue
}
}
}
}
开始就是从光标位置入手,获取换行符
接着为了保险,再判断一下是不是换行符,记住这里得是Char类型
如果是的话就得判断插入换行的位置,如果是在末尾,那就将前面的文本存起来,在末尾添加两个换行和手打八个空格(空格的处理如果有好方法麻烦留在评论区)
如果光标不在末尾就得确保它不是在文本最开始的位置,因为刚刚也说了,在最开始的地方插入换行有点问题。
与末尾添加换行不同的是,如果是在文中添加换行,就得保证光标的位置,所以
val newTextLength = newText.length
是在中间获取的,而不是等他拼好之后再获取位置。
最后就是将新拼好的文本传给变化后的文本和全局变量就好了!
贴一下整体代码:
private var remValTextBefore: TextFieldValue? = null
private var remValTextAfter: TextFieldValue? = null
fun onInput(
value: TextFieldValue,
contentText: MutableState,
) {
if (remValTextBefore == null) {
remValTextBefore = value
} else {
if (remValTextAfter == null) {
remValTextAfter = value
}
}
if (remValTextAfter != null) {
remValTextAfter = value
// 1、文本发生变化触发
if (remValTextBefore!!.text != remValTextAfter!!.text) {
var realText = value
// 2、文本长度变长才会触发,这种情况下才有可能是插入了换行
if (remValTextAfter!!.text.length > remValTextBefore!!.text.length) {
// 3、变化后的文本起始和终止位置相同,说明不是选了一段文本或是粘贴了一段文本
// 这时有可能是插入换行,也可能是插入了新内容
if (remValTextAfter!!.selection.start == remValTextAfter!!.selection.end) {
// 4、判断插入换行的位置:
// a、文本开头,b、文本中间,c、文本末尾
// 如何判断:根据文本变化前后的selection的start或end,此时应该都是一样的
val enterSymbolChar = remValTextAfter!!.text.substring(
remValTextAfter!!.selection.start - 1,
remValTextAfter!!.selection.start
).toCharArray()
if (enterSymbolChar.size == 1) {
if (enterSymbolChar[0] == '\n') {
// c:
if (remValTextAfter!!.selection.start > remValTextBefore!!.selection.start) {
val frontText = remValTextAfter!!.text.substring(
0, remValTextAfter!!.selection.start - 1
)
val newText = StringBuilder()
newText.append(frontText)
newText.append('\n')
newText.append('\n')
newText.append(" ")
val newTextFieldValue =
value.copy(
newText.toString(),
TextRange(newText.length)
)
realText = newTextFieldValue
remValTextAfter = newTextFieldValue
}
// b:
if (remValTextAfter!!.selection.start <= remValTextBefore!!.selection.start
&& remValTextAfter!!.selection.start > 1
) {
val frontText = remValTextAfter!!.text.substring(
0, remValTextAfter!!.selection.start - 1
)
val backText = remValTextAfter!!.text.substring(
remValTextAfter!!.selection.start,
remValTextAfter!!.text.length
)
val newText = StringBuilder()
newText.append(frontText)
newText.append('\n')
newText.append('\n')
newText.append(" ")
val newTextLength = newText.length
newText.append(backText)
val newTextFieldValue =
value.copy(newText.toString(), TextRange(newTextLength))
realText = newTextFieldValue
remValTextAfter = newTextFieldValue
}
}
}
}
contentText.value = realText
} else {
contentText.value = realText
}
// 只适用于中文以及不需要拼写的语言
remValTextBefore =
TextFieldValue(remValTextAfter!!.text, TextRange(remValTextAfter!!.text.length))
} else {
contentText.value = value
}
}
else {
contentText.value = value
}
}
还有一点小问题,就是新建章节时自动插入一行空格,这个很好解决:
在按钮功能里加几句就好了,当然别忘了,新建新书的时候也会新建一章:
这下就大功告成了!
最后就是把它用起来了,先是在初始化的地方:
val textControl = remember { TextControl() }
然后是输入框控件处:
而且要注意自定义控件中对value的类型修改:
再来个预览吧: