这篇工作总结,主要针对移动端网页开发中遇到的软键盘坑来整理的。
最近在做移动端页面,然后遇到一个很多人都遇到的坑:当页面有固定在底部的评论框的情况下,当点击评论框获得聚焦之后,底部评论框会被软键盘顶起,但是不会贴着软键盘顶部,而且后面的页面也会向上滚动一定高度,而且仍然可以滚动。这个问题的结果就是,在评论框获得聚焦时,整个页面会滚动,输入框位置很突兀,如果输入框是textarea,则有可能有一部分推到页面顶部一样,用户体验非常差。
这个问题拆解出来,有几个需要解决的问题:
背景页面仍然可以滚动。我尝试了增加遮罩层,但是还是不受控制;
底部评论框顶起的位置不正确。这个问题在安卓系统不会出现,所以是iOS的坑;
底部评论框获得聚焦的时候,背景页面会被顶起。
这个坑我在网上找了很多帖子,发现真的是一个很难解决的问题。我也有找到一些解决方案,但是实际实现下来效果还是很差。这里我们一个个问题来整理如何解决。
背景页面滚动问题
这个问题不管在安卓还是iOS系统上都会出现。原因应该出现在本身页面高度超过窗口高度,而且移动端有touchmove事件,虽然有遮罩层,但是其实body是遮罩层的根节点,所以存在事件捕获跟冒泡的问题。
因此,主要的问题是要阻止touchmove事件。解决方案需要完成两步操作:
1. 禁止body的touch事件。这样可以确保touchmove事件不会从body元素开始捕获
处理的办法是设置body元素的touchAction属性为none
:
1 | document.body.style.touchAction = 'none'; |
这样就可以禁止body的touch事件。不过这个只是适用于安卓系统,在safari上并不适用,因为safari不支持该属性
2. 阻止body发生默认的行为。这样可以保证touchmove事件不会触发body元素的滚动
处理的办法是使用e.preventDefault()
,同时在绑定addEventListener
的时候,需要再传一个options
。
1 | document.body.addEventListener('touchmove', this.stopTouch, { passive: false, capture: true }); |
addEventListener
的第三个参数options
的写法比较少用到,我们一般用到的是useCapture
,而且一般也是使用默认值false
。在MDN有详细的说明:
1 | options |
我们在这里使用{ passive: false }
,这样使用preventDefault()
时Chrome等一些浏览器就不会报错;而使用{ capture: true }
则可以是事件尽早被阻止。
第一个问题完整的解决方案代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19watch: {
show(newVal) {
this.toggleContainerTouchAction(newVal);
if (newVal) {
document.body.addEventListener('touchmove', this.stopTouch, { passive: false, capture: true });
} else {
document.body.removeEventListener('touchmove', this.stopTouch, { capture: true });
}
}
}
method: {
toggleContainerTouchAction (flag) {
document.body.style.touchAction = flag ? 'none' : 'auto';
},
stopTouch (e) {
e.preventDefault();
},
}
底部评论框顶起的位置
首先,这个问题的根本问题是iOS在处理fixed布局时,当页面输入框获得焦点的时候,会把fixed改成absolute绝对定位,同时计算fixed容器在页面的位置,以此作为绝对定位的位置。正常来说其实会让底部评论框顶到软键盘顶部的位置,虽然如果滚动页面的话底部评论框是会跟软键盘分开,但是上面已经有解决方案了,所以其实还好。问题在于因为在软键盘上滑的同时,页面整体也会被动向上滚动,但评论框的位置是在页面滚动前计算的,所以才会出现顶起后还跟软键盘分离的情况。
因此,首先可以想到的是,不用fixed布局,而改用absolute布局,同时在输入框获得焦点的时候,动态计算输入框容器的高度。
不过直接放弃fixed布局其实是很可惜的,因为如果一开始写布局的时候就用绝对定位,那底部评论框就得一直动态计算位置,而且这个问题只是iOS系统才会出现,安卓系统是没问题的。
所以在处理的时候,我把问题拆分了:
- 输入框在布局上,iOS跟安卓是分开处理的;
- 其次,iOS系统的布局,也按软键盘调起(等同于输入框获得焦点)与否分开处理;
- 因为页面滚动已经被设置为不可滚动,所以动态计算可以控制在一定次数以内。这三个优化可以让方案实施时减少JS的操作,提高页面性能,增加CSS样式操作。
1. 样式布局
样式布局上,我把fixed布局跟absolute布局从原来class.input-container
中分离出来:
1 | .input-container-absolute { |
然后通过v-bind
先把.input-container-fixed
绑到输入框容器上。当输入框获得焦点的时候,判断平台是iOS还是安卓,如果是iOS,就改成.input-container-absolute
,如果是安卓,就不变:
1 | handleComment () { |
当输入框失去焦点的时候,再改回fixed布局:
1 | handleBlur () { |
这样对于安卓系统来说,解决iOS系统的js操作跟安卓系统来说一点影响都没有。
2. iOS系统的布局
其实上面的代码也基本说明了,在iOS系统上,布局也不是完全使用绝对布局,就上面的代码,就可以保证在软键盘没有被调起的时候,不需要js控制布局。
3. 动态计算
这个问题是整个问题的核心点。首先,使用什么值来确定输入框的位置。因为使用了绝对定位,而且这个位置问题其实是y轴上滚动造成的,所以问题缩小为top
跟bottom
的选择上。一般fixed布局使用bottom
,绝对定位也使用bottom
可以统一定位方式,但是实际在.input-container-fixed
跟.input-container-absolute
的样式上,其实已经说明了绝对定位时,我是选择top
来控制输入框的位置的。因为我在尝试使用bottom
,发现计算不出来,尴尬…
所以我是计算了top
。怎么计算?我们这么考虑,应该是这样的公式:
1 | top = 页面滚动高度 + 视窗高度 - 输入框容器高度 |
首先,页面滚动高度可以通过document.documentElement.scrollTop
或document.body.scrollTop
获得;输入框是在软键盘的顶部,而且其实当软键盘调起之后,实际的视窗高度是原来视窗高度减去软键盘高度。这部分不需要计算,直接使用window.innerHeight
;输入框容器高度可以通过offsetHeight
获得。因此,公式转换成:
1 | top = document.documentElement.scrollTop/document.body.scrollTop + window.innerHeight - this.$refs.input.offsetHeight |
这里简单说一下,bottom的计算我就是用上面的公式,就是不减去容器高度而已,但是显示效果就是不对…
所以就有下面的代码:
1 | getScrollTop () { |
其实直接使用上面的代码也不能解决,因为软键盘弹出是有时间的,也就是如果一获取焦点就计算,那就会导致输入框还在软键盘弹出前视窗的底部。所以要动态计算,即在一定时间内,需要使用定时器setInterval
不断计算输入框的位置,这样在软键盘调起的过程,输入框也能一直附在软键盘顶部:
1 | scrollContent () { |
这里控制了只计算20次,是因为软键盘被调起的时间其实很短,测试情况大概1秒内就可以完成,不用一直动态计算,而且定时器间隔时间是50,所以就计算为20次。
因此,样式布局的方法就得增加一些代码:
1 | clearSrcoll() { |
这里要说明的是,跟业务代码是有不同的,因为实际上页面一开始放的评论框容器没有input标签,只是放了一个像输入框的底部容器,加了点击事件后,会控制显示输入框容器的显示,然后在DOM刷新之后(this.$nextTick
)使用focus()
触发输入框获得焦点。所以就不放完整代码了。
页面顶起
这个问题其实没有解决,考虑到影响不大,而且当页面高度只有一屏时,也不会出现(主要是没有找到很好的解决方案),所以就不处理了。但是这个问题在其他情况下有解决方案,详情可以看工作总结(五)中关于iOS下微信H5页面的坑解决方案的整理。
上面就是全部内容。