前言
本文将围绕以下问题进行展开:
- 页面加载自动聚焦
- ios与Android机型软键盘表现差异很大,输入、换行问题兼容
- ios的页面滚动问题及占位文本的展示问题
- 如何更好地控制光标的位置
页面加载自动聚焦
说明: 这里说的是没有任何用户行为前,有用户操作的自动聚焦无需处理,本身支持。
首先,我们想到的是使用focus()来实现聚焦,然而,当在真机测试时却发现根本没效果
解决方案
ios手机
ios系统默认不支持事件自动聚焦,所以这个需要客户端同学的支持,使webview允许事件自动聚焦。
这样我们页面使用focus()会直接生效。
Android手机
安卓客户端的webview无法通过事件自动聚焦并拉起键盘,但是可以通过原生代码实现,添加原生方法:webview.requestFocus()
然后暴露给h5一个方法即可,在需要聚焦的地方直接调用。
关于ios和Android软键盘的表现
ios软键盘(如下左图)
- 输入框获取焦点,键盘弹起
- 页面并没有被压缩,或者说高度(height)没有改变
- 可见区发生了变化,即原页面高度-软键盘高度
- ios软键盘其实是脱离页面视图层的,可当作两层看,键盘对页面高度没有影响
- 软键盘收起,输入框失去焦点
Android软键盘(如下右图)
- 输入框获取焦点,键盘弹起
- 页面高度会发生改变
- 可视区同样发生变化
- 软键盘与页面共用一个视图层,整体高度固定,键盘弹起,页面自然被压缩
- 软键盘收起,输入框仍然聚焦
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jq95zniA-1656592274534)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c3e79a0176354b009d69f3499e3a6bac~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ffvKAVnu-1656592274534)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b67822caced74e0d8c7533fa80b3822e~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
通过两端软键盘的表现,我们可以得知,键盘拉起,两端的可视区都发生了改变,即visualViewport发生了变化。
developer.mozilla.org/zh-CN/docs/…
通过下图可以比较清晰地看出两者的区别。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-626WX6XB-1656592274535)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f6204fe5d45043578930f7cdfd4aa742~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)] ios的问题
- ios输入内容高度超过可视区时,页面不会自动滚动,导致超出区域被隐藏
- 输入框重新聚焦时,如果位于可视区内,则页面不动,但是如果聚焦位置超出可视区,页面会自动滚动,导致页面导航区域上移,影响交互体验。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fVkfqtNF-1656592274535)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/73b612e74fec43698ff0061fb82863b0~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qebuDgrQ-1656592274535)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/614ff5659146447a8a73e28953068362~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
Andorid的问题
- 连续输入多行,光标自动定位在最下方,输入框上移。🆗的
- 当再次聚焦时,光标被隐藏,无法自动定位到光标处并显示在可视区。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5hf4Inxd-1656592274535)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cb2ded1a46d84f869143e2228ff2e962~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iuM6cpVE-1656592274536)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/826f27c2c40f40a2b0e264471abd0619~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
如何获取可视区高度
其实上面提到过,键盘拉起后,他们的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,要解决光标滚到可视区,必须要知道光标的坐标。
直接获取光标位置目前无法做到,但是我们可以变通一下,基本思路是:
在屏幕外创建一个伪
页面输入区:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ecUkkpcs-1656592274536)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4f34192c11d4eaf9f806fe68ab945c4~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
下图为对应的伪div的区域,此处是为了演示方便使其可见,实际使用时是不可见的 visibility为hidden,并且获取光标坐标后自动从body移除。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hZNsK1mr-1656592274536)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4022bc555f614f5bae366b521c3a2aff~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image)]
这里可以直接使用textarea-caret库,有兴趣可以参见源码。
可获取光标相对于文本区域顶部及左边的像素距离。使用示例如下:
import getCaretCoordinates from 'textarea-caret'
const {top} = getCaretCoordinates(el, el.selectionEnd) // selectionEnd指选区结束的位置 结束在第几个字
Andorid机型解决方案
如何让光标自动定位到可视区域呢?
方案
监听键盘拉起时,将光标所在位置移动到可视区域,其实就是滚动textarea区域到输入位置,我们将光标定位在可视区最后一行即可。
我们可以回到上面的gif动图,页面收缩后,变化的区域只有textarea,高度变小,输入内容超过它的高度后会出现滚动条。那么我们可以得到如下结论:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YSU0EUqd-1656592274536)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d03f5391b09d4cf6bbce42cef78df581~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
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)
实现效果如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z5NdQfTI-1656592274537)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9867e4bdd9ce4074b7767521241f1ecf~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
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都存在。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SUoDsnII-1656592274537)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/26ce7b8732ad46679115ced0e5e36211~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
这里我们可以定义一个是否需要滚动的变量。 如果当前事件为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
}
}
}
本文总结了WebView在iOS和Android上遇到的输入、光标定位问题及其解决策略。针对页面自动聚焦、软键盘表现、可视区高度变化、光标坐标获取等问题,提供了针对性的解决方案,包括Android和iOS机型的特定处理方法,确保光标始终可见并优化用户体验。
1175

被折叠的 条评论
为什么被折叠?



