相信大家写原生小程序都遇到过一个问题,当输入框聚焦键盘弹起时,页面会自动上推,使得输入框刚好位于键盘之上,在安卓中推动的只是内容,但在ios中,推动的是整个页面,导致导航栏被推出屏幕外,如下:
针对这个问题,目前的解决方案是将自动上推改成手动上推,让我们自己来控制页面内容的滚动。
一、方案一
1.取消自动上推
微信小程序中的input和textarea都有一个属性adjust-position,将其改为false
2.添加类名或者id
我们给每个输入框或者需要定位到键盘之上的元素添加唯一类名或者id,另外,我们还要给input或textarea添加自定义属性,值也为同一个类名或者id。
如上图,我期望键盘弹起能刚好将整个输入栏顶在键盘之上,所以我选择给这一栏加上唯一类名,里面的input自定义属性值为该输入栏的唯一类名,这样做事为了当我触发键盘事件时,能拿到当前输入栏的类名,获取该元素的坐标信息。
3.绑定键盘事件
input和textarea,微信小程序官方提供了键盘弹起的事件
这个方法里面的逻辑是本次的重点,主要是计算手动推动距离,先看代码:
代码语言:txt复制// 监听页面软键盘弹起手动推动页面
bindkeyboardheightchange(e) {
// 键盘高度
const height = e.detail.height;
const className = e.target.dataset.class;
if (height === 0) {
this.scrollToInput(0);
return;
}
try {
this.createSelectorQuery()
.select(`.${className}`)
.boundingClientRect((res) => {
const windowHeight = this.data.windowHeight;
// 除去键盘的剩余高度
let restHeight = windowHeight - height;
// 元素左下角坐标
let bottom = res.bottom;
// 只有当元素被软键盘覆盖的时候才上推页面
if (bottom <= restHeight) return;
// 现阶段需要滚动的大小
let scrollTop = bottom - restHeight;
this.scrollToInput(height, scrollTop);
})
.exec();
} catch (error) {}
}
// 获取页面滚动条位置
getScrollOffset() {
return new Promise((resolve) => {
try {
wx.createSelectorQuery()
.selectViewport()
.scrollOffset((res) => {
resolve(res.scrollTop);
})
.exec();
} catch (error) {
resolve(0);
}
});
}
// 监听页面软键盘弹起手动推动页面
scrollToInput(keyboardHeight, scrollTop) {
this.setData({
keyboardHeight,
});
if (scrollTop) {
try {
this.getScrollOffset().then((lastScrollTop) => {
wx.pageScrollTo({
// 如果已经存在滚动,在此基础上继续滚
scrollTop: lastScrollTop ? lastScrollTop scrollTop : scrollTop,
duration: 300,
});
});
} catch (error) {}
}
}
这里涉及到几个值,参见下图:
注意:这里的页面使用的是原生导航栏,若使用的是自定义导航栏,那么B/D/E/H都会再加上G区域,E/H在官方文档有说到,是元素基于显示区域的坐标位置。
- 键盘弹起后,获取到键盘的高度
C
,用显示区域B
减去键盘区域C
就是我们可使用的区域D
- 获取输入栏底部距离显示区域的坐标,如
E/H
- 若输入栏底部坐标小于可使用区域
D
,如H
,则说明当键盘弹起时,该输入栏不会被键盘遮挡,不需要推动 - 反之,若大于
D
,如E
,则说明键盘弹起时,输入栏会被键盘遮挡,这个时候就需要页面上推至输入栏完全展示出来 - 针对4,将
E
减去D
,得到一个差值F
,这就是当前元素距离完全展示还需要滚动的距离 - 页面实际滚动距离应该为
F
加上页面之前已经有的滚动距离,所以在滚动之前,需要再获取一次当前页面的滚动距离 - 这里可能会存在一个问题,页面的高度不够,无法滚动这么长的距离,因此,当键盘弹起时,这里需要给页面增加高度,这里直接是增加的键盘高度
<view style="height:{{keyboardHeight}}px"></view>
到这里,我们就已经实现了页面自动上推的功能了。另外,这里可以根据实际情况来做个判断,一般情况下,安卓我们可以直接使用原生的推动,即adjust-position为true,ios使用手动上推。
最终实现效果如下:
二、方案二
有些手机或者版本过低,监听不到键盘事件,可以使用聚焦事件和失焦事件代替,事件对象中也返回了键盘的高度。
步骤和逻辑处理与方案一相同的,如下:
代码语言:txt复制// 监听页面软键盘弹起手动推动页面
bindfocus(e) {
const height = e.detail.height;
const className = e.target.dataset.class;
if (height === 0) {
this.scrollToInput(0);
return;
}
try {
this.createSelectorQuery()
.select(`.${className}`)
.boundingClientRect((res) => {
......
this.scrollToInput(height, scrollTop);
})
.exec();
} catch (error) {}
}
bindblur(e) {
this.scrollToInput(0);
}
对比两种方案 1. 肉眼观察,方案一的推动是及时的,方案二有一点点延迟,如下: 方案一方案二调试发现,他们的触发时机和滚动时机都差不多,但是键盘事件触发多次,而聚焦和失焦只会触发一次,大胆猜测,这可能就是上述问题的原因 2. 方案一键盘事件触发多次,可能每次获取到的高度和元素bottom不同,从而导致多次滚动,这里可以使用节流获取到第一次的数据即可 大家根据自己的需求选择使用哪一种方案
三、疑难杂症
在一些特殊的场景下,还会有各种奇奇怪怪的问题
1、问题:在方案一中,如果textarea
展示了原生完成,在点击完成时,或者失焦键盘落下事件未监听到
解决:配合bindblur
或者bindconfirm
,将keyboardHeight
设为0
// 监听页面软键盘弹起手动推动页面
scrollToInput(keyboardHeight, scrollTop) {
this.setData({
keyboardHeight,
});
if (scrollTop) {
......
}
}
bindblur(e) {
this.scrollToInput(0);
}
bindconfirm() {
this.scrollToInput(0);
}
2、问题:获取元素的坐标时,会默认保留全部小数,我们都知道,js在计算的时候会存在精度问题,有可能会滚动错误
解决:获取到元素坐标后,最好只保留两位小数,计算时注意处理精度
3、问题:当页面同时有input
和textarea
时,若只给textarea
绑定键盘事件,input
会触发该textarea
的键盘事件
解决1:使用方案二
解决2:某些特殊情况,可以将textarea
隐藏,不要在键盘弹起时让input
和textarea
同时存在页面中,那么input
的键盘事件触发后,可能依然会触发textarea
的事件,但这个时候由于textarea
隐藏了,获取的键盘高度为0,所以还是会以input
的键盘事件为准
4、问题:bindkeyboardheightchange
会触发多次,某些特殊情况中,每次的高度获取不一致,导致滚动多次
解决1:使用方案二
解决2:打印每次获取的高度,看哪一次是对的,使用节流或者防抖获取正确的数据
5、问题:当页面同时有input
和textarea
,并且textarea
添加了原生的完成那栏,先点击textarea
触发键盘事件,再点击input
触发键盘事件,input
获取到的键盘高度是有完成那栏的,导致页面上推距离不准
解决:不要使用原生的完成,自定义一个完成,键盘弹起时将他使用动画移动到键盘之上,这个时候记得在计算D
区域的时候,要减去自定义完成栏的高度
如果非要用原生的完成,可以参考一下这个方法:使用方案一,bindkeyboardheightchange
事件添加防抖,获取到真实的键盘高度,页面中添加两个变量,一个是input
的高度,一个是textarea
的高度,当输入框聚焦获取到键盘高度时,判断当前类型的高度是否有值,没有就赋值,有就用之前的值
const height = e.detail.height;
const type = e.target.dataset.type;
this.data[type] = this.data[type] || height;