如何用 js 获取虚拟键盘高度?-前端早读课

3,455 阅读9分钟
原文链接: mp.weixin.qq.com

前言

9月7号早读文章由@汤谷投稿分享。

正文从这开始~

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

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

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

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

以往的解决方案

以往的解决方案:

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

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

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

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

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

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

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

IOING 的解决方案分析

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

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

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

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

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

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

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

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

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

先来看一下下面的图片

当键盘弹出时,键盘高度 = 不可见窗口高度

这个等式是有条件的,只有当 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)})

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

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

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

当 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 获取键盘高度的方法

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

最后

IOING 是一个渐进式 Web App 开发引擎,为你的 SPA 产品实施性能策略和组件化方案。这个解决方案IOING的文档,可通过文末“阅读原文”查看。

关于本文

作者:@汤谷

原文:https://segmentfault.com/a/1190000010693229