海阔天空的云

我们在自己的世界里独自狂欢

0%

薛定谔的导航栏


需求

自从在本单位工作之后,大部分时候做的都是移动开发。因此,也就不可避免地会遇到很多移动开发的坑。这些坑来源于真实的业务场景,因此,简单总结一下。

第一个坑是关于导航栏的,我们知道市面上的大部分国产APP,它的iOS版本和Android版本,在UI设计上几乎都没有区别,我们做的APP也是如此。因此,也是会在APP的底部有一个导航栏。

这个时候,有意思的事情就出现了。如果是H5页面的话,通常情况下,这个底部导航栏都是通过fixed定位来实现的。而由于移动终端虚拟键盘的作用,Android和iOS对此的反应又并不相同,因此,就出现了这个坑。

我看了很多APP,比如美团,微信,孩子王等APP,当光标聚集到输入框上面的时候,往往会有一个动画,然后将这个搜索页全屏处理。且不论这些APP是否是内嵌H5的,但这样的交互,如果在H5上面也如是一样,就能避免上面提到的由于fixed定位产生的坑。

这个坑的详细表现

在iOS上面的表现:

软键盘唤起后,页面的 fixed 元素将失效(即无法浮动,也可以理解为变成了 absolute 定位),所以当页面超过一屏且滚动时,失效的 fixed 元素就会跟随滚动了。( 来自Web移动端Fixed布局的解决方案

而在Android上面的表现是:

软键盘唤起后,页面的 fixed 元素并没有失效,但是视窗的高度将重新被计算。(原来的视窗高度-虚拟键盘的高度)

所以,如果我们仍然要把底部导航栏和元素放在一个页面上,就需要着手进行改造。不然,就非常影响用户体验。

解决方案

Web移动端Fixed布局的解决方案 提到的当然是一种解决方案,但是,我试着总是不能符合我的要求。

这就说会到我这篇文的题目《薛定谔的导航栏》,我的想法是这样的:如果弹出了虚拟键盘,则将导航栏的样式设置为 display:none,反之,如果隐藏了虚拟键盘,则将导航栏的样式设置为display:block; position:fixed。所以,一个小白,如果看到了弹出虚拟键盘的页面,你问他,现在到底页面上存在不存在导航栏呢?

假如他回答存在,你把这个虚拟键盘隐藏掉,结果当然是有的。可是当时有没有呢?并没有吧。

加入他回答不存在,你把这个虚拟键盘隐藏点,结果却是有的,很明显又和实际看到的不一样了。

知识点

所以接下来和核心问题,就是如何在iOS上和Android上去检测虚拟键盘的弹出和隐藏了。

下面进入直接贴代码说明问题环节,这里要说明一下,由于我使用了React框架,因此,实际上,我下面写的代码都是JSX文件的一部分

iOS

addIosKeyboardHandler = () => {  // IOS上,弹出输入框后,同样隐藏底部导航。
    document.addEventListener('focus', this.IosFocusHandler, true);
    document.addEventListener('focusout', this.focusoutHandler, true);
}

removeKeyboardHandler = () => {
    document.removeEventListener('focus', this.IosFocusHandler, true);
    document.removeEventListener('focusout', this.focusoutHandler, true);
}

IosFocusHandler = () => {
    if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
        window.scrollTo(0, 0);
        const footer = document.getElementById('footer');
        footer.classList.remove('fixed');
        footer.classList.add('hidden');
        window.setTimeout(() => {
            document.activeElement.scrollIntoViewIfNeeded();
        }, 100);
    }
}
focusoutHandler =() => {
    window.scrollTo(0, 0);
    const footer = document.getElementById('footer');
    footer.classList.add('fixed');
    footer.classList.remove('hidden');
}

这里有几个点需要注意:

  • focus事件需要通过捕获(capture: true)的形式才能触发,在document上面的监听
  • focusout要比blur事件在描述虚拟键盘隐藏这件事情上更加准确,blur事件不会冒泡
  • 当弹出输入法准备敲击时,先将页面滚动到顶部

Android

Android 上和IOS上又有一些不同,主要的不同点在于:当弹出虚拟键盘后,通过点击返回键,隐藏掉虚拟键盘,这个时候,如果我们对此不做任何处理的话,那么光标还是会在输入框里面,换句话说,此时对于输入框俩说,并没有发生blur事件,因为很显然,光标还在输入框里面。

因此还是根据Android上的表现来做判断,既然弹出虚拟键盘会让视窗的高度变小,而收起虚拟键盘又会让视窗的高度变大,那么我们就从这里着手。

addAndroidKeyboardHandler = () => {
    this.maxHeight = document.documentElement.clientHeight;
    window.addEventListener('resize', this.AndroidKeyboardHandler.bind(this, this.maxHeight));
    document.addEventListener('focusout', this.focusoutHandler, true);
}

removeAndroidKeyboardHandler = () => {
    window.removeEventListener('resize', this.AndroidKeyboardHandler.bind(this, this.maxHeight));
    document.removeEventListener('focusout', this.focusoutHandler, true);
}

AndroidKeyboardHandler = maxHeight => { // 解决 Android 弹出输入框时,底部导航一同浮起的问题。隐藏底部导航
    const presentHeight = window.innerHeight;
    const footer = document.getElementById('footer');
    if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
        if (presentHeight < maxHeight) { // 隐藏底部导航
            footer.classList.remove('fixed');
            footer.classList.add('hidden');
            window.setTimeout(() => {
                document.activeElement.scrollIntoViewIfNeeded();
            }, 100);
        } else {
            document.activeElement.blur();
            footer.classList.add('fixed');
            footer.classList.remove('hidden');
        }
    }
}

小提示

这个时候只需要把我上面代码中的addIosKeyboardHandler 方法在componentDidMount的时候,进行触发即可。

当然,这里可能要补充一个小知识点,由于componentDidMount的时候,通过React渲染的dom已经处于loaded的状态,因此无需再像以往通过window.onload或者jq的$(document).ready去添加事件处理程序。

下面的问题

这样的方法可以解决大部分的问题。但是还是会有一些历史遗留代码与此方案相处地并不融洽。

比如在购物车页面,会有一个输入框,可以改变选定商品的数量。购物车的结算button是fixed的定位,当时考虑到在他下面还有导航栏,因此,提前计算好了导航栏的高度。于是这个部分的样式大概是这样的:

  display: fixed;
  bottom: 1.4rem;

恩,我们使用了rem布局。结果就会造成由于我前面的处理,只会在出现虚拟键盘的时候将footer(底部导航栏)隐藏掉,并不会对结算button进行处理,因此,如果看Android上的表现,这个时候,这个button所在的部分就会和键盘之间有1.4rem的间距。而诡异的是,结算button部分还是处于fixed的状态,而这,并非我所期待的。

这就是另外一个问题了。

所以,还是说回来。这也是为什么之前看到其他一些人有一些友情提示:在做移动开发的时候,不要将input元素和其他fixed的元素放在一起。而像我这种更要命,我直接在页面底部放了两个fixed的元素。

我目前的措施是针对这种情况,单独处理。毕竟,这也只是特例。