二十一、使用Jetpack Compsoe编写一个写小说的Android应用:【TextField应用】文字滚动与键盘适配

这次的是基础功能实现,所以也算是初版吧,能提供个参考。

这个功能也是在最后才发现的,有如下现象:

1、初始效果

1.1 获取焦点时有问题

1.2 不跟随光标移动

经过百度发现这是TextField的问题,目前好像并没有解决,当然我是在自定义TextField上展示的,如果你用原始的TextField也是一样的效果。

2、实现效果

那么先看下实现完成后的效果:

3、功能实现

接下来就要确定思路了:

其实主要发生错误移动是在文本超出屏幕时,当你的文本很短,不会被软键盘遮挡时,是不会看到任何变化的,所以,我们重心集中研究多行时的变化:

1、首先我们要能获取每一行的位置,这样才能进行后续的操作

2、获取位置后,判断光标所在的行

3、将屏幕自动滚动到对应的行

4、根据行高对滚动进行微调

3.1行定义

创建行函数scrollAutomatically:

Kotlin 复制代码
fun scrollAutomatically(
    contentText: MutableState<TextFieldValue>,
    style: TextStyle, density: Density,
    maxWidthInPx: Int, fontFamilyResolver: FontFamily.Resolver, scope: CoroutineScope,
    scrollState: ScrollState,
) {
    val textLayoutResult =
        Paragraph(
            text = contentText.value.text,
            style = style,
            density = density,
            constraints = Constraints(maxWidth = maxWidthInPx),
            fontFamilyResolver = fontFamilyResolver
        )

其中的参数后面会提及,想跟着思路往下走。

在研究段落文字的时候,偶然发现了Paragraph这个属性,正好能解决这个问题。

先看它列出来的这几个参数

text不用说,就是存放整个输入框内容的 style就是定义内容的字体,行高之类的 density这个是屏幕像素浓度?之类的,就是用来将我们使用的dp啥的转化成px像素的 constraints这个是屏幕宽度,或者说是你这个控件的宽度,它会根据这个宽度来换行,也会用这个宽度来计算行数 fontFamilyResolver 不清楚,但是要有

有了这个变量之后,我们就能获取到行的一些属性了。

3.2获取行属性

先上代码

Kotlin 复制代码
if (textLayoutResult.lineCount > 1) {
        val lineHeight = textLayoutResult.getLineHeight(1).roundToInt()
        val firstLineHeight = textLayoutResult.getLineHeight(0).roundToInt()
        // 第一行高102px 后续行高105px
        // 1231是根据scrollLineBottomPx - scrollState.maxValue计算出来的补偿值,推测是软键盘高度,但是获取这个高度比较繁琐,先不搞了
        val scrollLineBottomPx =
            textLayoutResult.getLineForOffset(contentText.value.selection.start) * lineHeight + firstLineHeight - 1231
        val scrollLineTopPx =
            textLayoutResult.getLineForOffset(contentText.value.selection.start) * lineHeight + firstLineHeight - 1231 - lineHeight
        // 显示区只能显示14行,总计1470px,而要除去自身那行就剩下13行,同时三分之一的行高是发现不美观,所以稍微多往下移动一点
        scope.launch {
            // 只在键盘挡住字时上移
            if (scrollLineBottomPx - scrollState.value > 0) {

                scrollState.animateScrollTo(scrollLineBottomPx)

                return@launch
            }
            // 只在上面的工具栏挡住一半字体的时候下移
            if (scrollState.value - scrollLineTopPx >= 1470) {
                scrollState.animateScrollTo(13 * lineHeight + scrollLineBottomPx - lineHeight / 3)
                return@launch
            } else {
                scrollState.animateScrollTo(scrollState.value)
            }
        }
    }else{
        scope.launch {
            scrollState.animateScrollTo(0)
        }
    }

首先通过lineCount 来获取行的总数,注意这个行数跟刚刚的constraints中的最大宽度有关系;

Kotlin 复制代码
val lineHeight = textLayoutResult.getLineHeight(1).roundToInt()
        val firstLineHeight = textLayoutResult.getLineHeight(0).roundToInt()

接着声明了2个变量lineHeight和firstLineHeight ,这是因为我在调试中发现第一行是102px,后续行都是105px,其实就差了3像素,统一成105px也可以的,而且这个行高是和像素高度挂钩的,我的行高是1.5em,要注意你自己的行高设置。

Kotlin 复制代码
val scrollLineBottomPx =
            textLayoutResult.getLineForOffset(contentText.value.selection.start) * lineHeight + firstLineHeight - 1231
        val scrollLineTopPx =
            textLayoutResult.getLineForOffset(contentText.value.selection.start) * lineHeight + firstLineHeight - 1231 - lineHeight
        

然后用getLineForOffset这个函数可以获取到光标所在的行数,再乘以一个行高,就能转化为像素值

其中的1231是我试出来的一个软键盘高度,之后再改,软键盘高度大概在1300px左右,之后想办法用代码获得,之前查了以下好像挺麻烦。

而在scrollState中,传递的是一个像素值,所以这也是我们之前为啥要转化成像素的原因,因为我们最终的落脚点就在scrollState上,我们要通过控制scrollState来滚动整个Column。

而观察显示区,只能显示14行文字:

所以,针对特定一行,看示意图

Kotlin 复制代码
if (scrollLineBottomPx - scrollState.value > 0) {

                scrollState.animateScrollTo(scrollLineBottomPx)

                return@launch
            }

假设紫色块是软键盘,那么软键盘的上边缘就是像素为0的地方(好像是,你们试试,不影响),所以scrollLineBottomPx - scrollState.value >0的话,说明被遮挡了,就将列表滚动到scrollLineBottomPx位置。

而当我们点击上方被遮挡的行时:

Kotlin 复制代码
if (scrollState.value - scrollLineTopPx >= 1470) {
                scrollState.animateScrollTo(13 * lineHeight + scrollLineBottomPx - lineHeight / 3)
                return@launch
            } else {
                scrollState.animateScrollTo(scrollState.value)
            }

它应该也要往上滚一点,露出整行,此时这行的top px一定是大于14行的第一行toppx的,所以通过这样的判断就可以确定上面的行有没有被遮挡。

而且往上滚是像素减小,所以是减lineHeight /3,这个数是随便定的,写一个固定的数也行。

这样整个函数就写完了:

Kotlin 复制代码
fun scrollAutomatically(
    contentText: MutableState<TextFieldValue>,
    style: TextStyle, density: Density,
    maxWidthInPx: Int, fontFamilyResolver: FontFamily.Resolver, scope: CoroutineScope,
    scrollState: ScrollState,
) {
    val textLayoutResult =
        Paragraph(
            text = contentText.value.text,
            style = style,
            density = density,
            constraints = Constraints(maxWidth = maxWidthInPx),
            fontFamilyResolver = fontFamilyResolver
        )
    if (textLayoutResult.lineCount > 1) {
        val lineHeight = textLayoutResult.getLineHeight(1).roundToInt()
        val firstLineHeight = textLayoutResult.getLineHeight(0).roundToInt()
        // 第一行高102px 后续行高105px
        // 1231是根据scrollLineBottomPx - scrollState.maxValue计算出来的补偿值,推测是软键盘高度,但是获取这个高度比较繁琐,先不搞了
        val scrollLineBottomPx =
            textLayoutResult.getLineForOffset(contentText.value.selection.start) * lineHeight + firstLineHeight - 1231
        val scrollLineTopPx =
            textLayoutResult.getLineForOffset(contentText.value.selection.start) * lineHeight + firstLineHeight - 1231 - lineHeight
        // 显示区只能显示14行,总计1470px,而要除去自身那行就剩下13行,同时三分之一的行高是发现不美观,所以稍微多往下移动一点
        scope.launch {
            // 只在键盘挡住字时上移
            if (scrollLineBottomPx - scrollState.value > 0) {

                scrollState.animateScrollTo(scrollLineBottomPx)

                return@launch
            }
            // 只在上面的工具栏挡住一半字体的时候下移
            if (scrollState.value - scrollLineTopPx >= 1470) {
                scrollState.animateScrollTo(13 * lineHeight + scrollLineBottomPx - lineHeight / 3)
                return@launch
            } else {
                scrollState.animateScrollTo(scrollState.value)
            }
        }
    }else{
        scope.launch {
            scrollState.animateScrollTo(0)
        }
    }
}

4、功能实装

这时基本已经完成了滚动功能,只需要将它加到控件上就行了

首先在WriteTextPage中声明一些变量

Kotlin 复制代码
val focusManager = LocalFocusManager.current
    val scrollState = rememberScrollState()
    val style = TextStyle.Default.copy(
        fontSize = 20.sp,
        lineHeight = 1.5.em
    )
    val fontFamilyResolver = LocalFontFamilyResolver.current
    val configuration = LocalConfiguration.current
    val density = LocalDensity.current
    val maxWidthInPx = (configuration.screenWidthDp.dp - 40.dp).dpToPx()

接着给布局添加垂直滚动

给控件添加函数

因为我们想让这个函数持续起作用,所以就可以放在焦点函数中,这个函数会在控件获得焦点时触发,而我们写的条件又是如果它获取焦点的条件,所以会一直触发。

这样就大功告成了!研究了一周多才研究出来的,可以的话点点关注和赞!

到这里为止,前面提到的基础功能就全部实现了,后续还会实现一些其他功能,敬请催更!

相关推荐
Indoraptor1 小时前
Android Fence 同步框架
android
峥嵘life1 小时前
DeepSeek本地搭建 和 Android
android
叶羽西1 小时前
Android14 Camera框架中Jpeg流buffer大小的计算
android·安卓
jiasting1 小时前
Android 中 如何监控 某个磁盘有哪些进程或线程在持续的读写
android
AnalogElectronic4 小时前
问题记录,在使用android studio 构建项目时遇到的问题
android·ide·android studio
我爱松子鱼5 小时前
mysql之InnoDB Buffer Pool 深度解析与性能优化
android·mysql·性能优化
江上清风山间明月8 小时前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
子非衣11 小时前
MySQL修改JSON格式数据示例
android·mysql·json
有点感觉13 小时前
Android级联选择器,下拉菜单
kotlin
openinstall全渠道统计14 小时前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos