问题症状
今天在开发一个移动端的 H5 页面时, 遇到了 iOS 上键盘收起时界面无法归位的问题. 下面详细描述下问题和症状:
页面结构
出问题的页面是一个表单结构. 即类似于一个 div 下有 4 个 input 表单的结构, 用于用户填写邮寄信息. 类似:
- <div>
- <input type="text" placeholder="请填写省市县" />
- <input type="text" placeholder="请填写地址" />
- <input type="text" placeholder="请填写姓名" />
- <input type="text" placeholder="请填写联系电话" />
- </div>
截图如下:
键盘弹起时页面自动上移
当用户在手机上输入联系电话时, iPhone 键盘会弹出, 此时 iPhone 上为了让用户可以看到电话输入框, 会将整个页面整体向上移动 (不然键盘会遮住电话输入框). 此时, 实际上页面顶部是离开了我们的视口一部分距离的 (我们看到界面中消失了一行输入框).
键盘收起时页面无法还原归位
然而当用户输入完成关闭键盘后, 键盘虽然收起了, 但页面位置却不会还原.
问题分析
实际上这是由于 iOS 无法在键盘收起时, 页面滚出视口的部分没有掉下来导致的. 这时用户是可以通过手指将页面拖回来的.
但是毕竟体验不好.
要解决这个问题, 我们可以在用户光标离开输入框的时候, 调用 Windows.scrollTo(0, 0) 来把页面滚动到跟视口顶部对齐, 从而实现页面归位的效果.
那么现在问题就是要给表单中 4 个输入框全部加上 blur 事件, 然后在 handler 中调用 Windows.scrollTo. 不过, 无论是通过 vue 的 @blur 还是通过 DOM 操作的方式添加, 都要添加 4 个事件监听, 不是很优雅. 很自然, 我们想到用事件代理.
事件代理
即, 我们把事件监听放到顶部元素上; 然后定义一个 inputBlur 的函数等待触发.
- <div @blur="inputBlur">
- <input type="text" placeholder="请填写省市县" />
- <input type="text" placeholder="请填写地址" />
- <input type="text" placeholder="请填写姓名" />
- <input type="text" placeholder="请填写联系电话" />
- </div>
结果, 发现我们的事件监听器无法触发. 原因经查是输入框的 blur 事件无法冒泡.
无法冒泡的解决方案
经过查询, 发现 focus 和 blur 两个 DOM 事件在规范中就是无法冒泡的. 而与之相类似的有另外 2 个事件 focusin 和 focusout 则是可以冒泡的.
网上一些文章提到 focusin 和 focusout 是 IE 浏览器才支持的一种 DOM 事件. 而实际上我们看 MDN 文档发现, 这两个事件已经成为 DOM 3 规范的一个标准, 而且可支持的浏览器数量并不少.
所以, 果断通过这两个事件解决问题, 我们改成 focusout
- <div @focusout="inputBlur">
- <input type="text" placeholder="请填写省市县" />
- <input type="text" placeholder="请填写地址" />
- <input type="text" placeholder="请填写姓名" />
- <input type="text" placeholder="请填写联系电话" />
- </div>
然后, 实现我们的事件处理器:
- inputBlur(e) {
- // 首先, 判断触发事件的目标元素是否是 input 输入框, 我们只关注输入框的行为.
- if (e && e.target && e.target.tagName && e.target.tagName.toLowerCase() === 'input') {
- Windows.scrollTo(0,0);
- }
- },
这时, 我们问题得到解决了, 当从输入框输入内容, 然后点击键盘的完成收起键盘, 效果符合我们的预期.
但是经过手机测试发现, 当我们从 电话输入框 直接切换到 姓名输入框 这种操作时, 页面会发生抖动. 我们来继续分析.
解决抖动问题
其实 2 个输入框切换时 抖动的原因也很简单. 因为我们在上述两个输入框之间切换时, 页面会首先触发 电话输入框 的 blur 事件, 接着触发 姓名输入框 的 focus 事件. 这样的话, 在 blur 时会触发我们的 Windows.scrollTo(0,0) 导致页面往下滚一下, 接着 姓名输入框 聚焦, 于是键盘继续弹起 --- 这导致页面再次向上移动.
其实, 在两个输入框之间切换这种操作时, 我们就没必要触发第一个输入框 blur 时的 Windows.scrollTo 行为了. 因此看我们修改下我们的代码, 让输入框切换这种操作发生时, 可以切断第一个输入框的行为. 这里我们用 setTimeout 来解决:
- <div @focusout="inputBlur" @focusin="inputFocus">
- <input type="text" placeholder="请填写省市县" />
- <input type="text" placeholder="请填写地址" />
- <input type="text" placeholder="请填写姓名" />
- <input type="text" placeholder="请填写联系电话" />
- </div>
- inputBlur(e) {
- // 首先, 判断触发事件的目标元素是否是 input 输入框, 我们只关注输入框的行为.
- if (e && e.target && e.target.tagName && e.target.tagName.toLowerCase() === 'input') {
- // 输入框失去焦点, 要把 iOS 键盘推出页面的滚动部分还原. 即将页面滚动到视窗顶部对齐
- console.log('设置 timer')
- this.timer = setTimeout(() => {
- console.log('timer 触发')
- Windows.scrollTo(0,0);
- }, 0)
- }
- },
- inputFocus(e) {
- // 如果 focus, 则移除上一个输入框的 timer
- if (e && e.target && e.target.tagName && e.target.tagName.toLowerCase() === 'input') {
- clearTimeout(this.timer);
- }
- }
完
来源: https://segmentfault.com/a/1190000019781137