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

接着给布局添加垂直滚动

给控件添加函数

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

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

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

相关推荐
一笑的小酒馆9 分钟前
Android性能优化之截屏时黑屏卡顿问题
android
懒人村杂货铺3 小时前
Android BLE 扫描完整实战
android
TeleostNaCl5 小时前
如何安装 Google 通用的驱动以便使用 ADB 和 Fastboot 调试(Bootloader)设备
android·经验分享·adb·android studio·android-studio·android runtime
fatiaozhang95276 小时前
中国移动浪潮云电脑CD1000-系统全分区备份包-可瑞芯微工具刷机-可救砖
android·网络·电脑·电视盒子·刷机固件·机顶盒刷机
低调小一6 小时前
Swift 语法学习指南 - 与 Kotlin 对比
微信·kotlin·swift
2501_915918417 小时前
iOS 开发全流程实战 基于 uni-app 的 iOS 应用开发、打包、测试与上架流程详解
android·ios·小程序·https·uni-app·iphone·webview
lichong9517 小时前
【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之dist打包发布在Android工程asserts里
android·vue.js·iphone
Android出海7 小时前
Android 15重磅升级:16KB内存页机制详解与适配指南
android·人工智能·新媒体运营·产品运营·内容运营
一只修仙的猿7 小时前
毕业三年后,我离职了
android·面试
编程乐学7 小时前
安卓非原创--基于Android Studio 实现的新闻App
android·ide·android studio·移动端开发·安卓大作业·新闻app