这次的是基础功能实现,所以也算是初版吧,能提供个参考。
这个功能也是在最后才发现的,有如下现象:
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()
接着给布局添加垂直滚动
给控件添加函数
因为我们想让这个函数持续起作用,所以就可以放在焦点函数中,这个函数会在控件获得焦点时触发,而我们写的条件又是如果它获取焦点的条件,所以会一直触发。
这样就大功告成了!研究了一周多才研究出来的,可以的话点点关注和赞!
到这里为止,前面提到的基础功能就全部实现了,后续还会实现一些其他功能,敬请催更!