二十一、使用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()

接着给布局添加垂直滚动

给控件添加函数

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

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

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

相关推荐
闲暇部落1 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX3 小时前
Android 分区相关介绍
android
大白要努力!4 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee4 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood4 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-7 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen9 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年17 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX17 小时前
kotlin
开发语言·kotlin
建群新人小猿19 小时前
会员等级经验问题
android·开发语言·前端·javascript·php