十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行

写小说的时候分行其实挺重要的,一大段文字挨在一块属实看不下去,别说读者了就是自己看起来都很费力,所以每次插入回车时多插入一行是很有必要的。这次时偏向于算法的实现,有点烧脑可能。

先看效果:

十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行_第1张图片

当然我写的这个程序有个问题,就是在最开始的地方插入换行的时候很不稳定:

 十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行_第2张图片

所以这个问题更适合在初始化的时候解决,先一步步来看!

还是老样子,梳理思路,

实现这个功能首先就是插入一个回车,这时候要拦截这个回车做一些修改,那么从哪里获取换行符呢?

没错,是在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两种

拼写是在英文拼写单词时触发的一种特别状态,这里不解释了,我也没用过而且如果加上它的话会大幅度增加文本的处理难度。

具体在后面用的时候就知道了!

1、外层框架

我们将其分成两块,一块是外层的逻辑,另一块是对文本的处理。

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,也就是说,如果不符合我们要处理的情况,那就要直接给全局变量赋值,这样才能保证文本框的值一直在更新。

2、文本处理

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
        }

    }

还有一点小问题,就是新建章节时自动插入一行空格,这个很好解决:

十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行_第3张图片

在按钮功能里加几句就好了,当然别忘了,新建新书的时候也会新建一章:

十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行_第4张图片

这下就大功告成了!

最后就是把它用起来了,先是在初始化的地方:

val textControl = remember { TextControl() }

十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行_第5张图片

然后是输入框控件处:

 十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行_第6张图片

而且要注意自定义控件中对value的类型修改:

十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行_第7张图片 

再来个预览吧:

十九、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】插入回车时多插入一行_第8张图片

你可能感兴趣的:(Android,android,kotlin,android,jetpack)