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

接着给布局添加垂直滚动

给控件添加函数

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

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

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

相关推荐
程序员勋勋2 小时前
安卓Android压力测试与性能测试详解!
android·压力测试
编程乐学2 小时前
网络资源模板--Android Studio 零食工坊(商城)
android·android studio·商城·大作业·安卓课设·购物商城
洞窝技术3 小时前
Android 15适配方案及常见问题
android
人工智能的苟富贵3 小时前
全面解析 iOS 和 Android 内嵌 H5 页面通信与交互实现方案
android·javascript·ios·交互
机器之心3 小时前
迈向多语言医疗大模型:大规模预训练语料、开源模型与全面基准测试
android·人工智能
夜未央ぴ陌上花开丶3 小时前
Android mqttv5
android
草明3 小时前
AndroidManifest.xml 文件中的 package 属性不再是强制要求定义
android·xml·react native
匆忙拥挤repeat4 小时前
Android 简单实现联系人列表+字母索引效果
android
Studying 开龙wu5 小时前
4.3章节python中循环结构:两种类型:for 循环和 while 循环用法
android·java·python
千里马学框架6 小时前
android锁屏界面userActivity自动息屏深入剖析
android·智能手机·framework·锁屏·车机车载·息屏·useractivity