H5 软键盘兼容方案

在 App 内嵌 WebView 的聊天、评论、搜索等场景中,页面通常由三部分组成:顶部 Header(固定)中间对话区(可滚动)底部输入框(固定)

理想状态下,软键盘弹出时只有中间对话区被压缩,Header 始终吸顶,输入框随键盘上推。但在 iOS 和 Android 上,实际情况往往与预期不符。本文整理了实际开发中常见的兼容问题,以及对应的处理方案。

一、理想布局

核心诉求可以概括为:键盘只影响中间内容区的高度,不改变 Header 和输入框的相对位置关系。

二、iOS

问题一:fixed 定位的 Header 跟着页面上滑

现象

输入框获得焦点后,Header 虽然设置了 position: fixed,但键盘弹出时仍会跟着整个页面一起滑走,无法保持吸顶。

原因

2015 年 3 月 iOS 8.2 发布后,键盘弹起时 WebView 不会被压缩,而是整体被顶上去。此时 fixed 元素不会重新计算定位,仍保持原来的相对位置,于是跟着页面一起上移。

解决方案

方案一:客户端原生 Header(推荐)

由客户端提供自定义 Header,或针对当前场景定制原生导航栏。原生 Header 不属于 WebView 内容,WebView 上推时它依然保持吸顶——这是从根源上规避问题的方式。

方案二:H5 动态修正 Header 定位

既然页面高度没变、只是整体上移,那么获取键盘高度后重新设置 Header 的 top 即可。

获取键盘高度有两种方式:

方式 原理 局限
window.pageYOffset 页面上移的距离即键盘高度 通用性好
VisualViewport API 用 WebView 高度减去可视区域高度 iOS 13+ 才支持

推荐第一种方式。处理时机是输入框 focus 时——iOS 键盘弹出有约 300ms 延迟,需要 setTimeout 配合:

function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}

const HEADER_SELECTOR = '.page-header';

function fixHeaderPosition() {
const header = document.querySelector(HEADER_SELECTOR);
if (!header) return;
header.style.top = `${window.pageYOffset}px`;
}

function resetHeaderPosition() {
window.scrollTo(0, 0);
const header = document.querySelector(HEADER_SELECTOR);
if (header) header.style.top = '0';
}

// 组件内
function onInputFocus() {
if (!isIOS()) return;
setTimeout(fixHeaderPosition, 350);
}

function onInputBlur() {
if (!isIOS()) return;
resetHeaderPosition();
}

iPad 横竖屏切换

iPad 翻转屏幕时,如果不重新计算 Header 定位,页面会出现错乱。监听 resize 让输入框失焦,聚焦时再重新计算:

window.addEventListener('resize', () => {
if (isIOS()) {
inputEl.blur();
}
});

参考:可能这些是你想要的 H5 软键盘兼容方案

已知缺点

切换键盘类型(如中英文切换)时,无法检测到键盘高度变化,页面会有轻微上下跳动。如需更平滑,可在 focus 期间监听页面滚动,实时更新 Header 的 topblur 后恢复。

避坑:不要用 scrollTo 强拉页面

网上有一种做法是:键盘弹出后强制 scrollTo 把页面拉回来,再修改 body 高度:

let initialBodyHeight = 0;

function onInputFocusBad() {
initialBodyHeight = document.body.clientHeight;

if (!isIOS()) return;

setTimeout(() => {
document.body.scrollTop = document.body.scrollHeight;
window.scrollTo(0, document.body.scrollTop + 1);
document.body.scrollTop >= 1 && window.scrollTo(0, document.body.scrollTop - 1);
}, 0);

setTimeout(() => {
const keyboardHeight = window.innerHeight - window.visualViewport.height;
if (keyboardHeight > 0) {
document.body.style.height = `${initialBodyHeight - keyboardHeight}px`;
}
}, 300);
}

function onInputBlurBad() {
if (!isIOS()) return;

document.body.style.height = `${initialBodyHeight}px`;
setTimeout(() => {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0;
window.scrollTo(0, Math.max(scrollTop - 1, 0));
}, 200);
}

这种方式看似可行,但如果恰好在滑动页面时聚焦,scrollTo 会与 -webkit-overflow-scrolling: touch 冲突,导致滚动异常。处理滑动问题时,应尽量避开对该属性的影响——它能让滚动更流畅,建议保持开启:

-webkit-overflow-scrolling: touch;  /* 手指离开后继续惯性滚动 */
-webkit-overflow-scrolling: auto; /* 手指离开后立即停止 */

方案三:H5 + 客户端 SDK

客户端提供 SDK,实时通知前端键盘高度变化,前端据此动态设置 top。精度和实时性最好,但需要客户端配合。

问题二:iOS 12.4.1 键盘收起后页面不恢复

现象

fixed 定位容器内的 input 聚焦时软键盘占位,失焦后键盘消失但占位未释放,导致输入框无法再次输入。

解决方案

失焦时主动滚动页面,释放占位:

window.scrollTo(0, 0);

三、Android

Android 不会出现 Header 不固定的问题,但键盘弹出的表现分两种模式。

正常模式

键盘弹出时 WebView 被压缩,底部输入框自然上滑——最理想的状态,无需额外处理。

异常模式(华为等机型居多)

键盘弹出后页面毫无变化,WebView 没有被压缩,键盘直接覆盖在 H5 页面上,输入框被挡住。

原因尚不明确,且因为可视区没有变化,前端也无法直接获取键盘高度

解决方案

方案一:H5 按屏幕比例估算键盘高度

思路比较粗糙:根据不同设备的宽高比推算键盘大概高度,给一个固定值。以下系数来自常见机型的实测经验,上线前需按目标设备微调

  • 监听 resize 无效——异常机型可视区根本不改变
  • scrollIntoView 也无效——系统认为屏幕大小就是可视区,不会产生滚动

聚焦后等待 600ms,对比前后可视区高度,过滤掉正常机型(正常机型键盘弹出后可视区会减小,不需要处理):

// 宽高比下限 → 键盘高度系数(值越小,估算键盘越高)
const KEYBOARD_RATIO_MAP = [
[1.9, 1.835],
[1.8, 1.89],
[1.7, 1.945],
[1.6, 2.0],
[1.5, 2.055],
[1.4, 2.11],
[1.3, 2.165],
[1.2, 2.22],
[1.1, 2.275],
[1.0, 2.33],
[0.9, 2.4],
];

function getViewportSize() {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
};
}

function estimateKeyboardHeight(screenHeight, width, height) {
const aspectRatio = height / width;
if (aspectRatio > 2) return screenHeight / 1.78;

const ratio = KEYBOARD_RATIO_MAP.find(([min]) => aspectRatio >= min)?.[1] ?? 2.6;
return screenHeight / ratio;
}

let cachedScreenHeight = 0;

function onInputFocus() {
cachedScreenHeight = getViewportSize().height;

setTimeout(() => {
inputEl.scrollIntoView(false);

// 过滤正常机型:可视区高度有变化说明 WebView 已被压缩
const { width, height } = getViewportSize();
if (cachedScreenHeight !== height) {
cachedScreenHeight = 0;
return;
}

const inputTop = inputBarEl.getBoundingClientRect().top;
const keyboardHeight = estimateKeyboardHeight(cachedScreenHeight, width, height);

if (inputTop - keyboardHeight < 0) return;

document.body.style.height = `${cachedScreenHeight - (inputTop - keyboardHeight)}px`;
}, 600);
}

function onInputBlur() {
if (cachedScreenHeight === 0) return;

document.body.style.height = `${cachedScreenHeight}px`;
cachedScreenHeight = 0;
}

失焦时重置页面高度即可。上面示例用 inputElinputBarEl 代指输入框及其容器,Vue 项目中可对应 $refs.input$refs.inputBar

缺点:实现有投机成分,不够严谨,但能兼容大多数异常机型。

方案二:H5 + 客户端 SDK

与 iOS 方案三相同,由客户端实时通知键盘高度,前端动态调整布局——最理想的长期方案。

四、方案对比

方案 适用平台 精度 客户端成本 推荐度
原生 Header iOS 完美 ⭐⭐⭐⭐⭐
H5 修正 top iOS 较好 ⭐⭐⭐⭐
H5 估算键盘高度 Android 异常机型 一般 ⭐⭐⭐
客户端 SDK iOS / Android 最好 ⭐⭐⭐⭐⭐

五、总结

H5 软键盘兼容没有银弹,核心思路是:

  1. 能交给客户端的尽量交给客户端——原生 Header 或键盘高度 SDK 是最稳的方案
  2. iOS 的关键是补偿 pageYOffset——键盘顶起整个页面后,用 top 把 fixed 元素拉回来
  3. Android 异常机型只能曲线救国——按屏幕比例估算,先过滤正常机型再处理
  4. 避开 scrollTo + -webkit-overflow-scrolling: touch 的组合——会引发滚动冲突

如果业务对体验要求高,建议推动客户端提供键盘高度 SDK,前端统一监听、统一适配,一劳永逸。

参考资料

微信打赏