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

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

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

1、初始效果

1.1 获取焦点时有问题

1.2 不跟随光标移动

 

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

2、实现效果

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

 

3、功能实现

接下来就要确定思路了:

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

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

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

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

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

3.1行定义

创建行函数scrollAutomatically:

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获取行属性

先上代码

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中的最大宽度有关系;

val lineHeight = textLayoutResult.getLineHeight(1).roundToInt()
        val firstLineHeight = textLayoutResult.getLineHeight(0).roundToInt()

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

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行文字:

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

 

if (scrollLineBottomPx - scrollState.value > 0) {

                scrollState.animateScrollTo(scrollLineBottomPx)

                return@launch
            }

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

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

 

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,这个数是随便定的,写一个固定的数也行。

这样整个函数就写完了:

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中声明一些变量

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()

接着给布局添加垂直滚动

给控件添加函数

 

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

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

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

 

  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HO灵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值