如何用 js 获取虚拟键盘高度?(适用所有平台)

60

前言

这是一个存在很久的历史问题了,对于这样一个具有普遍性的问题浏览器偏偏没有给出解决方案,what?没有方案还聊个什么?

别急,别急,接下来我们一起来扒一扒关于软键盘高度和 input 的问题

我们先来看一个短片认识一下这个问题

js 获取软键盘高度,解决第三方输入法遮挡底部input及android键盘回落后留白问题

问题描述:当操作者进行输入操作的时候,弹起的软键盘把原本的输入框遮挡了,导致操作者看不到操作结果

以往的解决方案

以往的解决方案:

  1. 修改网站的页面布局,比如本例中 twitter 尽量把 input 放置在中部以上的位置,从布局上尽量避免此类问题

  2. 在一些指定设备和浏览器中异步获取 window.innerHeight 进行前后对比而得出键盘高度

再来看一下另一种常见输入框的页面布局:

js 获取软键盘高度,解决第三方输入法遮挡底部input及android键盘回落后留白问题

在这个场景里,输入框定位在页面的最底部,当软键盘弹起时整个视图窗口页面向上卷动,到达最底部时停止。恰巧当我们用 h5 来模拟这个效果的时候刚好勉强做到。

这是因为当你首次 fouse 到输入框的时候软键盘弹出,浏览器会使页面会向上滚动,以确保 input 是可见的,该特性和 document.body.scrollIntoViewIfNeeded 方法是一致的,但是当你 body 的可滚动高度超过窗口高度时还会产生另一个问题:固定元素将随页面滚动 如下图

js 获取软键盘高度,解决第三方输入法遮挡底部input及android键盘回落后留白问题

因此浏览器关心的只是 input 是否被覆盖?实际上是 input 中的光标位置!那么这就解释了为什么输入框在底部的时候刚好勉强完成了,因为 input 在页面的底部时,软键盘弹出势必会遮挡住 input,因而浏览器会向上滚动至输入框可见的位置。

但是如下图的效果这样就无法做到了,因为在输入框的下面还有一行工具栏,也就是说输入框并非在最底部的位置,那么浏览器在滚动到可视位置时只会确保到 input 可见,而对于工具栏是否可见则并不在浏览器的考虑范围内。

js 获取软键盘高度,解决第三方输入法遮挡底部input及android键盘回落后留白问题

IOING 的解决方案分析

综合来看上面两种布局方案的问题,都不能完美解决输入被键盘遮挡和底部 footer 不能被顶起的问题,那是不是就没得法子了?

当然号称可以让 HTML5 表现更接近 Native 的 IOING 引擎一定是有解决方案的

我们先来看一段 input 在 IOING 中的表现

解决第三方输入法遮挡底部input及android键盘回落后留白问题

解决第三方输入法遮挡底部input及android键盘回落后留白问题

我们可以看到在输入过程中页面通过滚动来始终保持光标位于可视区域的中心位置,因此在这里我们需要提一个知识点:获取输入光标的实时位置,当然这也是一个曲折的过程,在这里我就不扩算话题了,继续来讲原话题

前面说了三个主要的传统解决方案:

  1. 第一个是通过把 input 布局尽量放在页面顶部,显然这个不是我们想要的,否决掉

  2. 把 input 放在最底部,用来完成 footer 固定的效果,但是要局限页面高度不超过窗口高度,我们可以通过自制滚动控件来解除这个限制,那现在需要解决的技术点就变为实现一个模拟滚动控件

  3. 通过比对软键盘弹出前后的 window.innerHeight 的高度差来得到键盘高度,从而根据这个高度来实现底部定位和输入剧中,但是该方法局限于不同设备平台的支持

    综上所述我们总结一下我们要解决的思路和步骤

    先来看一下下面的图片

解决第三方输入法遮挡底部input及android键盘回落后留白问题

当键盘弹出时,键盘高度 = 不可见窗口高度
这个等式是有条件的,只有当 input 在对底部时该等式才成立 (这是上面讲过的 scrollIntoViewIfNeeded 的原因)

思考:如果我们能让该等式成立,且能够获取不可见位置高度,是否就能得出键盘高度了呢

我们整理好思路一步一步来实现

1.需要将内容放置在虚拟滚动中,在 IOING 像下面这样就可以创建一个虚拟滚动区域了

<scroll>
<scrolling>
    页面内容
</scrolling>
</scroll>

传统页面可以使用 WebKit 私有属性“-webkit-overflow-scrolling: touch” 来允许独立的滚动区域和触摸回弹,或者使用 iScroll.js 等第三方库来完成,但是需要注意对 iScroll 使用不当可能会造成性能问题

2.获取光标位于屏幕中的位置

3.当光标 fouce 时,键盘弹起,若 input 被遮挡页面会进行滚动,但滚动量不确定,因此我们可以强制滚动到底端,即键盘完全弹出后主动使窗口向上滚动窗口高度的距离,而实际上窗口只能向上滚动到最底部位置后就不能再向上滚动了,此时获取页面的 top.scrollY 即为实际键盘高度

得出公式:

可视区域的中心位置 = 键盘高度 + (窗口高 - 键盘高度)/2
应滚动距离 = 可视区域的中心位置 - 光标offsetTop - (光标被遮挡 ?键盘高度 :0)

当然实际操作需要更多的细节,po 出 IOING 中该部分逻辑实现的源代码:

// IOING 中部分源代码
// dom 为 input 元素
// scroll 为滚动容器的 Scroll 对象

function scrollTo (y, _y, t, s, r) {
    r = r == undefined ? 1 : r
    y = y == undefined ? top.scrollY : y
    if ( r == 1 ? y > _y : y < _y) return
    s = s == undefined ? Math.abs((_y - y) / t * 17.6) : s
    rAF(function () {
        top.scrollTo(0, y += r*s)
        scrollTo(y, _y, t, s, r)
    })
}

function visibility () {
    if ( this.moving || this.wheeling ) {
        var top = dom.offset().top
        var height = dom.offsetHeight
        var viewTop = keyboardHeight + scrollOffsetTop
        var viewBottom = factWindowHeight - scrollOffsetBottom

        if ( top + height <= viewTop || top >= viewBottom ) {
            dom.blur()
        }
    }
}

function refreshCursor () {
    rAF(function () {
        dom.getSelectionRangeInsert('')
    })
}

function getScroll () {
    var scroller = reactScroller || dom.closest('scroll')

    scroll = scroller ? scroller.scrollEvent : null

    if ( type == 1 ) {
        minScrollY = scroll.minScrollY
    }
}

function getViewOffset () {
    // android : (top.scrollY == 0 ? keyboardHeight : 0)
    viewOffset = viewCenter - rangeOffset.top - (top.scrollY == 0 ? keyboardHeight : 0) + (that.module.config.sandbox ? keyboardHeight : 0)
    
    return viewOffset
}

function keyboardUp (e) {
    getScroll(1)

    if ( !scroll ) return

    // refresh cursor {{

        if ( device.os.ios && device.os.iosVersion < 12 ) {
            scroll.on('scroll scrollend', refreshCursor)
        }

    // }}
    
    if ( normal ) return

    function upend (e) {

        window.keyboard.height = keyboardHeight = top.scrollY || factWindowHeight - top.innerHeight

        // change minScrollY

        scroll.minScrollY = minScrollY + keyboardHeight
        scroll.options.minScrollY = scroll.minScrollY

        // 光标位置
        
        rangeOffset = dom.getSelectionRangeOffset()

        // 可见视图的中心

        viewWrapper = factWindowHeight - keyboardHeight - scrollOffsetTop - scrollOffsetBottom
        viewCenter = keyboardHeight + viewWrapper / 2

        scroll.scrollBy(0, getViewOffset(), 600, null, false)

        // 滚动到不可见区域时 blur
        
        scroll.on('scroll', visibility)

        window.trigger('keyboardup', { 
            height : keyboardHeight 
        })

        if ( reactResize ) {
            scrollTo(null, 0, 300, null, -1)
        }
    }

    setTimeout(function () {

        top.one('scrollend', upend)

        // no scroll
        
        setTimeout(function () {
            if ( keyboardHeight == 0 ) upend() 
        }, 300)

        // ``` old
        
        var offset = 0

        if ( device.os.mobileSafari && device.os.iosVersion < 12 ) {
            offset = 24 * viewportScale
        }

        // scroll to bottom

        scrollTo(null, viewportHeight - offset, 300, null, 1)

    }, 300)
}

function keyboardDown () {
    getScroll()

    if ( !scroll ) return

    // ``` old : refresh cursor {{

        if ( device.os.ios && device.os.iosVersion < 11 ) {
            scroll.off('scroll scrollend', refreshCursor)
        }

    // }}

    if ( normal ) return
    if ( keyboardHeight == 0 ) return false

    top.scrollTo(0, 0)
    scroll.wrapper.scrollTop = 0
    
    // change minScrollY

    scroll.minScrollY = minScrollY
    scroll.options.minScrollY = minScrollY
    scroll.off('scroll', visibility)
    scroll._refresh()

    window.keyboard.height = keyboardHeight = 0
}

function selectionRange (e) {
    getScroll()

    if ( !scroll ) return

    // 非箭头按键取消
    
    if ( e.type == 'keyup' && ![8, 13, 37, 38, 39, 40].consistOf(e.keyCode) ) return

    // 重置光标位置

    if ( reactOffset ) {
        rangeOffset = dom.getSelectionRangeOffset()
    } else if ( reactPosition ) {
        rangeOffset = dom.getSelectionRangePosition()
    }

    if ( reactOrigin && rangeOffset ) {
        rangeOffset.each(function (i, v) {
            scope.setValueOfHref(reactOrigin + '.' + i, v)
        })
    }

    if ( normal ) return

    // 光标居中

    if ( e.type == 'input' && e.timeStamp - timeStamp < 2000 ) return
    if ( !scroll || !viewCenter ) return
    if ( !reactOffset ) {
        rangeOffset = dom.getSelectionRangeOffset()
    }

    timeStamp = e.timeStamp

    scroll.scrollBy(0, getViewOffset(), 400, null, false)
}

dom.on('click', checkChange)
dom.on('focus', keyboardUp)
dom.on('blur', keyboardDown)
dom.on('focus keyup input paste mouseup', selectionRange)
})

其它的小细节和注意事项:

  1. safari 会受到浏览器底部导航栏的影响,会产生20多像素误差,需要针对考虑

  2. safari 中的 input 光标在执行 transform 3d变换的时候会出现光标停滞的现象,需要执行光标刷新操作

  3. 当 input 被操作者主动滑出可视区域外时应处罚键盘收起操作,否则在输入时 scrollIntoViewIfNeeded 效应将导致窗口滚动出现空白的问题

最后总结:

获取键盘高度只是我们的表象,真正解决 html5 带来的各种问题才是我们的研究课题,也只有扫清这些布局杀手 h5 才能在追赶 Native 的道路上更近一步!


结尾

最后的最后我来 po 一下在 IOING 中完成这一步我们需要做什么?

<input placeholder=写点啥>

就是这么简单,IOING 中 input 默认就能拥有自动居中特性

如果你要取消这个特性,就像下面这样写

<input nomal placeholder=写点啥>

当然也可以设置居中相对底部/相对于顶部的偏移位置

<input scroll-offset-top=50 placeholder=写点啥>
<input scroll-offset-bottom=50 placeholder=写点啥>

在输入过程中能够实时输出光标位置,且将位置信息赋值给数据源对象

<textarea react-position="test.range" resize="none"></textarea>
<p>当前光标位置:left: {test.range.left}, top: {test.range.top}</p>
<!-- test.range 为一个数据源对象 -->
<!-- react-position 指令将把该输入框的光标状态传递给 test.range 对象  -->

用js 获取键盘高度的方法

//键盘弹起时为键盘高度,未弹起时为0
console.log(window.keyboard.height)
// 通过键盘弹起事件获取
window.on('keyboardup', function (e) {
    console.log(e.height)
})
// 键盘收起事件
window.on('keyboarddown', function (e) {
    console.log(e.height) // 0
})

详细文档传送门:http://ioing.com/#docs-dom-input
GitHub 传送门:https://github.com/ioing/IOING

你可能感兴趣的

24 条评论
weishijun14 · 2017年08月17日

看样子像vue,能和react一起用么

+1 回复

0

这是一个综合性的框架,可以单独使用,也可以和 vue以及react一起用

汤谷 作者 · 2017年08月17日
1

在下看着官网教程恁是没懂怎么跑起来。。。

pkjy · 2017年08月22日
0

@pkjy 刚更新了文档,可以使用命令行启动,你可以再看一下,有任何问题可以联系我微信 wechat:ioingroot

汤谷 作者 · 2017年08月30日
小沙 · 2017年08月17日

mark,没认真看但是觉得,肯定有用

回复

汤谷 作者 · 2017年08月17日

谢谢关注

回复

Geocld · 2017年08月17日

请问这个方案对第三方输入法有用吗

回复

0

适用所有输入法

汤谷 作者 · 2017年08月17日
0

@汤谷 修正一下,ios中某些输入法会存在由于工具栏部分的误差,但是添加主屏幕运行则不存在这个问题,IOING其实是希望用户能做混合开发

汤谷 作者 · 2017年08月18日
tanranran · 2017年08月17日

Android适用吗?

回复

0

web 全适用

汤谷 作者 · 2017年08月17日
bluefantasy728 · 2017年08月17日

安卓机上可以通过focus事件来提升整个页面高度从而避免被键盘遮挡,但是当输入完成后,如何触发键盘缩回的事件呢?

回复

0

主动blur以及进入不可见区域时收回

汤谷 作者 · 2017年08月17日
90arther · 2017年08月18日

“唤起键盘后,主动使窗口向上滚动窗口高度的距离即为键盘的高度。”这样有两个问题,第一个问题,键盘唤起有一定的时间(不同的第三方输入法时间不一样),而触发页面滚动的动作需要等键盘完全唤起后,唤起键盘这个时间长度不确定,就很容易造成页面卡顿的现象。第二个问题,ios下的第三方输入法(如搜狗)自带的工具栏也算在页面的窗口里面,所以页面即使主动滚动到最底部,也还是有一部分被输入法挡住。

回复

1

@90arther 键盘唤起时间从开始到结束是可以检测的,另外一些特别的输入法确实不能获得工具栏部分高度,谢谢你的反馈

汤谷 作者 · 2017年08月18日
sunfly1991 · 2017年08月18日

安卓弹出键盘的时候不是会出现有的把页面往上滚动,有的是直接覆盖页面的差异吗?

回复

0

input 在可见位置时也是直接覆盖的,但是页面可滚动区域是增加的,因此我们主动迫使页面滚动至最底部,直到滚不动为止

汤谷 作者 · 2017年08月18日
airyland · 2017年09月13日

找了找没有找到 demo。。

回复

0

使用方法在这里 http://ioing.com/#docs-dom-in...

汤谷 作者 · 2017年09月13日
韦字只念第二声 · 2017年09月14日

所以这个方法一定需要自己做一个虚拟滚动?虚拟滚动的坑不比弹出软键盘小唉……

回复

0

@韦字只念第二声 嗯,这个的前提是先解决虚拟滚动的坑

汤谷 作者 · 2017年09月14日
程龙 · 2017年12月21日

问下作者,我是这样做的
<style>

  html,body{
    /*height:70%;
    */
    /*border: 5px solid green;*/
  }
  .contain{
    width:100%;
    height:100%;
    border:1px solid black;
    position:absolute;
    top:0;
  }
  .header{
    position:absolute;
    top:0;
    left:0;
    width:100%;
    height:20px;
    background-color:blue;
    z-index:1;
  }
  #test{
    position:absolute;
    top:20px;
    bottom:30px;
    font-size:50px;
    width:100%;
    overflow:auto;
    border:1px solid black;
    -webkit-overflow-scrolling:touch;
  }
  .footer{
    position:absolute;
    bottom:0;
    left:0;
    width:100%;
    height:30px;
    background-color:red;
    z-index:1;
  }
  .footer input{
    position:absolute;
    bottom:0;
    left:0;
  }
  </style>

<body>

<div class = "contain">
<div class = "header"></div>
<div id = 'test'>
  <div>这是滚动内容</div>
</div>
<div class = "footer">
  <input type = "text"/>
</div>

</div>
</body>
我想做成以往的解决方案那样就可以,但是这样写在ios中使用搜狗输入法,头部会遮盖input框,能详细说下原来的具体解决方案吗?这个将来要给公司用的,拜托啦

回复

0

请问您是怎么解决的?

书生脚气 · 2018年09月03日
木子喵 · 2018年04月20日

键盘的高度 就是当前屏幕的高度-可是区域的高度
let scrollTop=document.body.scrollTop;

      let clientTop=document.body.clientHeight;
      console.log(scrollTop,"----底部---可视区域的高度",window.innerHeight,"屏幕高度-",clientTop);
      console.log("offsetHeight", document.body.offsetHeight);
      console.log("scrollTop", document.body.scrollTop);


      const keyboard = clientTop - window.innerHeight;
      console.log("keyboard",keyboard);

回复

载入中...