前言
“输入框被虚拟键盘遮挡” 的问题,不少同学都遇到过。尤其是对于页面尺寸固定,且输入框只能显示在下半部分,甚至边缘位置的情况,遮挡现象会成为阻挡用户获得确定性或安全感的一大障碍。
如果你想要简化问题处理,可以尝试与产品经理沟通,将输入框的呈现位置调整至页面的上半部分即可。但如果你并不想要绕过这个问题,想要和它磕一磕,就需要继续往下看,深入地了解这一问题。
对比
经对比,iPhone 上的输入框在键盘弹出时表现正常,会自动被顶到可见区域,不需要额外处理。需要处理的,是部分安卓手机。
通过对比不同手机的表现,发现键盘大致可以按两种形式弹出:“挤压窗口高度弹出” 和 “悬浮弹出”。
“挤压窗口高度弹出” 将缩小窗口的实际高度(window.innerHeight
变小,position
fixed
在底部的元素会跟着键盘被顶上去)。而 “悬浮弹出”时,窗口将保持原高度,键盘以悬浮在窗口上方的形式展现给用户。
挤压窗口高度式弹出
对于 “挤压窗口高度” 式弹出虚拟键盘的手机,例如:红米6pro,除厂商自己实现的安全键盘外,问题并不突出,只需在检测到输入框获得焦点时,调用输入框的 scrollIntoViewIfNeeded()
即可解决遮挡问题;
悬浮式弹出
而对于 “悬浮” 式弹出虚拟键盘的手机,例如:华为荣耀magic2,则在问题的表现上要严重的多,需要着重对待。
如果开发者调用
scrollIntoViewIfNeeded()
时页面没有任何改观,大概率是页面尺寸固定,没有更多拖动空间的原因。
对于这个问题,我最初按这个思路上进行尝试:
捕获 [键盘打开] 及 [键盘关闭] 事件,根据 [键盘高度] 动态调整页面的
paddingBottom
,然后再调用输入框的scrollIntoViewIfNeeded()
方法
但翻阅各种资料,都找不到这两个事件或者等价的其它事件,也找不到任何 API 使得我可以捕获键盘高度,抑或是其它可以探测键盘高度的方法。
接着,改造思路:
- 借助
blur()
和focus()
事件,假定 键盘已关闭 或 键盘已打开 - 硬编码一个较大的数值,如:200px,代表猜想的虚拟键盘的高度
- 在键盘打开时,动态调整页面的
paddingBottom
,使其等于猜想的键盘高度 - 调用输入框的
scrollInToViewIfNeeded()
- 当输入框焦点失去时,恢复页面的
paddingBottom
经过优化和调测,问题已大致解决。完整代码如下:
/**
* 解决部分安卓手机上,虚拟键盘会遮挡输入框的问题
*/
;(function(){
/**
* Win32 代表 windows 系统,说明当前是开发者工具调测环境,不需要处理
* MacIntel 代表 mac 系统,同样说明当前是开发者工具调测环境,不需要处理
* iPhone 上表现正常,不需要处理
*/
if(["Win32", "MacIntel", "iPhone"].indexOf(navigator.platform) !== -1)
return;
/**
* 文档正文是否加载完成
* @type {boolean}
*/
var isDomContentLoaded = false;
/**
* 没有虚拟键盘时,页面可以展现的最大高度
* @type {Number}
*/
var maxAvailableHeight = null;
/**
* 猜想的虚拟键盘的高度
* @type {number}
*/
var suspectedVirtualKeyboardHeight = 200;
/**
* 检测结果:虚拟键盘是否以“悬浮在正文内容上”的形式展现
* @type {boolean|null}
*/
var ifVirtualKeyboardOverlapsContent = null;
/**
* 焦点捕获/失去动作是否由内部触发,以重新捕获焦点
* @type {boolean}
*/
var isBlurredToReFocus = false,
isFocusedToReOpenKeyboard = false;
/**
* 虚拟键盘是否已经打开
* @type {boolean}
*/
var isVirtualKeyboardOpen = false;
var bakedPaddingBottom = null;
var docEle = document.documentElement;
/**
* 判断检测环境是否就绪
*/
var isEnvironmentReady = function(){
return isDomContentLoaded;
};
/**
* 刷新光标,重新捕获焦点,迫使可能关闭了的键盘重新出现,防止计算失误
* @param {Function} callback 重新聚焦后要执行的方法
*/
var reOpenKeyboardIfNeeded = function(callback){
var focusedObj = document.querySelector(":focus");
if(null == focusedObj){
if(null === maxAvailableHeight)
maxAvailableHeight = window.innerHeight;
return;
}
isBlurredToReFocus = true;
focusedObj.blur();
if(null === maxAvailableHeight)
maxAvailableHeight = window.innerHeight;
isFocusedToReOpenKeyboard = true;
focusedObj.focus();
setTimeout(function(){
isVirtualKeyboardOpen = true;
if(typeof callback === "function")
callback(focusedObj);
}, 200);
};
/**
* 检查虚拟键盘的展现方式,判断是否是悬浮在正文内容上
* 调用前提:虚拟键盘已经呈现
*
* @returns {boolean|undefined}
*/
var checkIfVirtualKeyboardOverlapsContent = function(){
if(null !== ifVirtualKeyboardOverlapsContent)
return ifVirtualKeyboardOverlapsContent;
return ifVirtualKeyboardOverlapsContent = window.innerHeight === maxAvailableHeight;
};
/**
* 将焦点所在的输入框滚动至可见区域内
*/
var scrollFocusedObjIntoView = function(){
if(!isEnvironmentReady()){
return;
}
reOpenKeyboardIfNeeded(function(focusedObj){
if(checkIfVirtualKeyboardOverlapsContent()){
bakedPaddingBottom = docEle.style.paddingBottom;
docEle.style.paddingBottom = suspectedVirtualKeyboardHeight + "px";
focusedObj.scrollIntoView();
}
});
};
document.addEventListener("DOMContentLoaded", function(){
isDomContentLoaded = true;
});
var _scrollFocusedObjIntoView = function(focusedObj){
if(null == focusedObj)
return;
/**
* 如果已经检测到“虚拟键盘的弹出不会遮挡正文内容”,则浏览器本身可能就不存在遮挡的问题,
* 或者遮挡的问题可以适当的通过手动的滚动页面解决
*/
if(ifVirtualKeyboardOverlapsContent === false){
if(focusedObj.scrollIntoViewIfNeeded)
focusedObj.scrollIntoViewIfNeeded();
return;
}
scrollFocusedObjIntoView();
};
var virtualKeyboardSwitchTimer;
docEle.addEventListener("focusin", function(e){
/**
* 工作一次就需要失效
*/
if(isFocusedToReOpenKeyboard === true){
isFocusedToReOpenKeyboard = false;
return;
}
clearTimeout(virtualKeyboardSwitchTimer);
if(isVirtualKeyboardOpen)
return;
var focusedObj = e.target;
_scrollFocusedObjIntoView(focusedObj);
});
docEle.addEventListener("focusout", function(e){
/**
* 工作一次就需要失效
*/
if(isBlurredToReFocus === true){
isBlurredToReFocus = false;
return;
}
clearTimeout(virtualKeyboardSwitchTimer);
virtualKeyboardSwitchTimer = setTimeout(function(){
isVirtualKeyboardOpen = false;
/**
* 如果键盘不会遮挡正文,则什么都不处理
*/
if(!checkIfVirtualKeyboardOverlapsContent())
return;
if(null !== bakedPaddingBottom)
docEle.style.paddingBottom = bakedPaddingBottom;
bakedPaddingBottom = null;
}, 10);
});
})();
虚拟键盘弹出后,如果用户点击页面的空白位置,或以其它形式使得输入框失去焦点,虚拟键盘关闭的同时,页面显示将复原。
但上述代码并不完美:如果用户点击键盘右上角的关闭按钮,页面是没有任何办法感知这一动作的,此时仍然以 “键盘已弹出” 的形态进行工作。因为输入框的焦点仍然还在(这一特性实在不该,应当失去焦点才对)。
虽然并没有完美解决遮挡问题,但一得一失之间,还是值得尝试的。