webview的输入及光标定位问题总结

您所在的位置:网站首页 获取光标所在位置 webview的输入及光标定位问题总结

webview的输入及光标定位问题总结

2023-06-18 10:28| 来源: 网络整理| 查看: 265

前言

本文将围绕以下问题进行展开:

页面加载自动聚焦 ios与Android机型软键盘表现差异很大,输入、换行问题兼容 ios的页面滚动问题及占位文本的展示问题 如何更好地控制光标的位置 页面加载自动聚焦

说明: 这里说的是没有任何用户行为前,有用户操作的自动聚焦无需处理,本身支持。

首先,我们想到的是使用focus()来实现聚焦,然而,当在真机测试时却发现根本没效果

解决方案

ios手机

ios系统默认不支持事件自动聚焦,所以这个需要客户端同学的支持,使webview允许事件自动聚焦。

这样我们页面使用focus()会直接生效。

Android手机

安卓客户端的webview无法通过事件自动聚焦并拉起键盘,但是可以通过原生代码实现,添加原生方法:webview.requestFocus()

然后暴露给h5一个方法即可,在需要聚焦的地方直接调用。

关于ios和Android软键盘的表现

ios软键盘(如下左图)

输入框获取焦点,键盘弹起 页面并没有被压缩,或者说高度(height)没有改变 可见区发生了变化,即原页面高度-软键盘高度 ios软键盘其实是脱离页面视图层的,可当作两层看,键盘对页面高度没有影响 软键盘收起,输入框失去焦点

Android软键盘(如下右图)

输入框获取焦点,键盘弹起 页面高度会发生改变 可视区同样发生变化 软键盘与页面共用一个视图层,整体高度固定,键盘弹起,页面自然被压缩 软键盘收起,输入框仍然聚焦

1.gif 2.gif

通过两端软键盘的表现,我们可以得知,键盘拉起,两端的可视区都发生了改变,即visualViewport发生了变化。

developer.mozilla.org/zh-CN/docs/…

通过下图可以比较清晰地看出两者的区别。

x.png ios的问题

ios输入内容高度超过可视区时,页面不会自动滚动,导致超出区域被隐藏 输入框重新聚焦时,如果位于可视区内,则页面不动,但是如果聚焦位置超出可视区,页面会自动滚动,导致页面导航区域上移,影响交互体验。

3.gif 4.gif

Andorid的问题

连续输入多行,光标自动定位在最下方,输入框上移。🆗的 当再次聚焦时,光标被隐藏,无法自动定位到光标处并显示在可视区。

5.gif 6.gif

如何获取可视区高度

其实上面提到过,键盘拉起后,他们的visualViewport(视觉视口)都会发生变化,我们可以直接通过浏览器自带的这个属性,获取视觉视口的宽高,即除去键盘高度的宽高。

window.visualViewport.height/window.visualViewport.width

那个键盘高度也就可以计算了

键盘高度 = 原页面高度 - 视觉视口高度

即: keyboardHeight = window.innerHeight - window.visualViewport.height

通过监听视口大小变化获取键盘高度

const handler = function() { if (!window.visualViewport) return console.log(window.innerHeight) // ios:812 Android:523 console.log(window.visualViewport.height) // ios:444 Android:523 const keyboardHeight = window.innerHeight - window.visualViewport.height } console.log(window.innerHeight) // ios:812 Android:804 console.log(window.visualViewport.height) // ios:812 Android:804 window.visualViewport.addEventListener('resize', handler) 复制代码

通过代码分析,可以发现,andorid的window.innerHeight也是变化的,即webview缩小。导致键盘高度计算出现问题。

我们可以优化一下代码,在页面加载的时候就保存页面高度,即初始高度。

class ScrollToCursor { constructor() { this.height = window.height this.keyboardHeight = 0 } setVisualView() { if (!window.visualViewport) return this.visualViewheight = window.visualViewport.height this.keyboardHeight = this.height - this.visualViewheight } } // 页面加载时 const instance = new ScrollToCursor() window.visualViewport.addEventListener('resize', () => instance.setVisualView()) 复制代码 如何获取光标的坐标位置(单位:像素)

无论是android还是ios,要解决光标滚到可视区,必须要知道光标的坐标。

直接获取光标位置目前无法做到,但是我们可以变通一下,基本思路是:

在屏幕外创建一个伪,其样式与文本区或输入完全相同。(从下图可以看出,宽高及内容区完全一致)然后,将插入符号之前的元素文本复制到div中,并在其后面插入一个。然后,将span的文本内容设置为中的其余文本,以便复制伪div中的换行(因为换行可以将当前输入的单词推到下一行) 。

页面输入区:

7.png

下图为对应的伪div的区域,此处是为了演示方便使其可见,实际使用时是不可见的 visibility为hidden,并且获取光标坐标后自动从body移除。

这里可以直接使用textarea-caret库,有兴趣可以参见源码。

可获取光标相对于文本区域顶部及左边的像素距离。使用示例如下:

import getCaretCoordinates from 'textarea-caret' const {top} = getCaretCoordinates(el, el.selectionEnd) // selectionEnd指选区结束的位置 结束在第几个字 复制代码 Andorid机型解决方案

如何让光标自动定位到可视区域呢?

方案

监听键盘拉起时,将光标所在位置移动到可视区域,其实就是滚动textarea区域到输入位置,我们将光标定位在可视区最后一行即可。

我们可以回到上面的gif动图,页面收缩后,变化的区域只有textarea,高度变小,输入内容超过它的高度后会出现滚动条。那么我们可以得到如下结论:

xx.png

textArea需要向上滚动的距离 scrollTop = caretTop - safeAreaHeight + lineHeight

这里面safeAreaHeight,我们也可以动态获取

safeAreaHeight = 页面高度 - textArea元素距离视口顶部距离 - textArea元素距离视口底部距离

代码实现如下:

import getCaretCoordinates from 'textarea-caret' // isAndroid 需要自己定义 class ScrollToCursor { constructor() { this.height = window.innerHeight this.visualViewheight = 0 this.safeAreaHeightAndroid = 0 // 安全输入区 this.keyboardHeight = 0 this.scrollDistanceAndroid = 0 this.viewTop = 0 // 文本输入框相对视口顶部距离 } setVisualView(el) { if (!window.visualViewport) return this.visualViewheight = window.visualViewport.height this.keyboardHeight = this.height - this.visualViewheight const elSize = el.getBoundingClientRect() this.viewTop = elSize.top const viewBottom = this.height - this.viewTop - elSize.height // 可输入区高度计算 this.safeAreaHeightAndroid = this.visualViewheight - this.viewTop - viewBottom if (isAndroid) { el.style.minHeight = this.safeAreaHeightAndroid + 'px' } // 键盘拉起执行滚动操作 if (this.keyboardHeight > 0) { this.inputHandler(el) } } inputHandler(el, e) { const { top } = getCaretCoordinates(el, el.selectionEnd) this.scrollDistanceAndroid = top - this.safeAreaHeightAndroid + 35 // Android文本区域大于文本可视区时 滚动textArea if (isAndroid && this.scrollDistanceAndroid > 0) { el.scrollTop = this.scrollDistanceAndroid } } } 复制代码

除了监听页面resize,在input及focus时也需要实时保证光标可见,即滚动到相应位置。

const instance = new ScrollToCursor() const handler = () => instance.setVisualView(el) // el即textArea元素 const getCursor = e => instance.inputHandler(el, e) el.addEventListener('input', getCursor) el.addEventListener('focus', getCursor) window.visualViewport.addEventListener('resize', handler) 复制代码

实现效果如下图所示:

8.gif

IOS机型解决方案 光标的问题

ios在输入的时候,超过可视区的光标不可见,不可见的原因就是页面高度不变,因此,文本输入区的高度不变,在其高度范围内,可以输入内容,而ios的键盘覆盖在页面上了,导致键盘部分的输入内容不可见。

方案

在键盘拉起时,改变文本框的高度,使其正好显示在键盘以上,然后滚动内容至光标所在位置,键盘收起时,为了保证页面的正确展示,将文本框高度恢复。

textArea需要向上滚动的距离 scrollTop = caretTop - safeAreaHeight + lineHeight

这里的区别在于可视区高度(safeAreaHeight)的不同。因为可视区内不包含底部的背景元素高度,所以

ios的可视区高度 safeAreaHeight = 页面高度 - textArea元素距离视口顶部距离

实现方式如下:

import getCaretCoordinates from 'textarea-caret' // isIos 需要自己定义 class ScrollToCursor { constructor() { this.height = window.innerHeight this.visualViewheight = 0 this.safeAreaHeightIos = 0 // 安全输入区 this.keyboardHeight = 0 this.scrollDistanceIos = 0 this.viewTop = 0 // 文本输入框相对视口顶部距离 } setVisualView(el) { if (!window.visualViewport) return this.visualViewheight = window.visualViewport.height this.keyboardHeight = this.height - this.visualViewheight const elSize = el.getBoundingClientRect() this.viewTop = elSize.top // 可输入区高度计算 this.safeAreaHeightIos = this.visualViewheight - this.viewTop if (isIos) { if (this.keyboardHeight) { // 键盘拉起 手动缩小可输入区 el.style.height = this.safeAreaHeightIos + 'px' } else { // 键盘收起 恢复高度 el.style.height = '100%' } } // 键盘拉起执行滚动操作 if (this.keyboardHeight > 0) { this.inputHandler(el) } } inputHandler(el, e) { const { top } = getCaretCoordinates(el, el.selectionEnd) this.scrollDistanceIos = top - this.safeAreaHeightIos + 25 // Ios文本区域大于文本可视区时 滚动textArea if (isIos) { if (this.scrollDistanceIos > 0) { el.scrollTop = this.scrollDistanceIos } } } } 复制代码 页面滚动问题

当光标位置位于键盘以上的区域时,页面不会发生滚动,但是如果光标位置位于键盘部分的区域时,页面会自动滚动到光标的位置。

此时的页面比较难看,头部都被隐藏掉了,我们希望页面不发生滚动,而光标也会自然出现在可视区。 我们设置文本区高度是在键盘拉起之后,但是滚动是与键盘拉起同时发生的。那怎么解决呢?

我的解决方案是,键盘拉起后,将页面滚动到顶部。

window.scrollTo(0, 0)

试了一下效果,页面正常滚到顶部,但是光标却没有准确出现在可视区。原因还是ios拉起键盘的时页面滚动的问题。当页面发生滚动,那么我们在计算元素的距视口距离就会发生变化(viewTop),那么得出可视区就会大于实际可视区,而我们又加了自定义滚动事件,并没有触发重新计算。

那么这时候ios就需要加一个scroll的监听事件,再次计算可视区并触发滚动。

代码如下:

const instance = new ScrollToCursor() const handler = () => instance.setVisualView(el) // el即textArea元素 const getCursor = e => instance.inputHandler(el, e) el.addEventListener('input', getCursor) el.addEventListener('focus', getCursor) window.visualViewport.addEventListener('resize', handler) window.visualViewport.addEventListener('scroll', handler) 复制代码 其他问题(失焦、滚动、占位显示) ios不能失焦 页面仍然可以滚动 placehodler内容超过两行,输入内容再删除,只显示一行(真是啥奇葩问题都有)

1和2加上相应的事件监听即可解决

代码如下:

const bodyScroll = e => { // 非内容区禁止滚动 if (e.target.tagName !== 'TEXTAREA') { e.preventDefault() } } const clickHandler = e => { // ios点击非输入区收起键盘 if (e.target.tagName !== 'TEXTAREA') { if (el.keyboardHeight > 0) { el.blur() } } } // 禁止页面滚动 针对ios拉起页面后 页面可滑动 ios需要显式增加passive参数 否则不生效 document.body.addEventListener('touchmove', bodyScroll, { passive: false }) document.body.addEventListener('click', clickHandler) 复制代码

问题3

监听输入内容的变化,当内容不为空时,随便设个值,不能为空;当内容为空时,设置为要展示的文案。

watch: { inputContent(val) { // fix ios 输入删除后 placeholder只显示一行 if (val === '') { this.placeHolder = 'text you need to display' } else { this.placeHolder = 'whatever' } } }, 复制代码 光标定位优化

当我已经到达可视区最底部后,换到上面的行输入,光标也自动定位到了最下方,这个问题ios和Android都存在。

9.gif

这里我们可以定义一个是否需要滚动的变量。 如果当前事件为input事件,并且光标的top值 < 上次定位的top值,是不需要滚动的。否则需要发生滚动行为。

那么滚动事件优化后的代码如下:

inputHandler(el, e) { const { top } = getCaretCoordinates(el, el.selectionEnd) this.scrollDistanceIos = top - this.safeAreaHeightIos + 25 this.scrollDistanceAndroid = top - this.safeAreaHeightAndroid + 35 let isScroll = true if (e && e.type === 'input') { // 正在输入的焦点在上次定位上面 不滚动输入区 正常输入 if (top > this.cursorTop) { isScroll = true this.cursorTop = top } else { isScroll = false } } if (isIos) { window.scrollTo(0, 0) // 修正整个页面往上移 仅内容区滚动 if (this.scrollDistanceIos > 0) { if (isScroll) { el.scrollTop = this.scrollDistanceIos } } } if (isAndroid && this.scrollDistanceAndroid > 0) { if (isScroll) { el.scrollTop = this.scrollDistanceAndroid } } } 复制代码


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3