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

本文总结了WebView在iOS和Android上遇到的输入、光标定位问题及其解决策略。针对页面自动聚焦、软键盘表现、可视区高度变化、光标坐标获取等问题,提供了针对性的解决方案,包括Android和iOS机型的特定处理方法,确保光标始终可见并优化用户体验。

前言

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

  • 页面加载自动聚焦
  • 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,要解决光标滚到可视区,必须要知道光标的坐标。

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

在屏幕外创建一个伪

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

页面输入区:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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) 
其他问题(失焦、滚动、占位显示)
  1. ios不能失焦
  2. 页面仍然可以滚动
  3. 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
      }
    }
  } 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值