软键盘常见问题(二)

22 阅读3分钟

前端 H5 软键盘常见问题及解决方案

1. 键盘弹起导致页面布局错乱 / 整体上推

问题:iOS Safari 中键盘弹起时,页面(body 或容器)会被整体上推,收起后页面可能无法回弹,出现底部留白。

解决方案:监听 visualViewport API 动态调整布局

// 利用 visualViewport 获取真实可视区域
function initVisualViewport() {
  if (!window.visualViewport) return;

  function onResize() {
    const { height } = window.visualViewport;
    document.documentElement.style.setProperty('--vh', `${height * 0.01}px`);
  }

  window.visualViewport.addEventListener('resize', onResize);
  window.visualViewport.addEventListener('scroll', onResize);
  onResize();
}

initVisualViewport();
/* 根元素使用动态视口单位 */
:root {
  --vh: 1vh;
}

.container {
  height: 100vh;
  height: calc(var(--vh, 1vh) * 100);
  /* 兼容不支持 visualViewport 的环境 */
}

也可以使用 window.visualViewport.offsetTop 判断是否被键盘遮挡,并手动 scrollTo


2. iOS 键盘收起后页面不回弹(留白问题)

问题:iOS 12+ Safari 中,输入框失焦后页面底部出现空白区域,滚动条位置异常。

解决方案一:滚动到顶部 / blur 时强制滚动

function fixIOSBlank() {
  const inputs = document.querySelectorAll('input, textarea');

  inputs.forEach(input => {
    input.addEventListener('blur', () => {
      // 延迟执行,等键盘收起动画结束
      setTimeout(() => {
        // 方案 A:滚动到顶部
        window.scrollTo(0, 0);
        document.body.scrollTop = 0;
        document.documentElement.scrollTop = 0;

        // 方案 B(可选):滚动到当前焦点元素
        // const active = document.activeElement;
        // if (active && active.scrollIntoView) {
        //   active.scrollIntoView({ behavior: 'smooth' });
        // }
      }, 100);
    });
  });
}

解决方案二:使用 scrollIntoView 配合 preventScroll

document.querySelectorAll('input, textarea').forEach(el => {
  el.addEventListener('focus', () => {
    // 让输入框滚入可视区,但不自动调整页面
    el.scrollIntoView({ block: 'center', behavior: 'smooth' });
  });
});

3. fixed 定位元素在键盘弹起时失效

问题:键盘弹起后,position: fixed 的元素(如底部工具栏)表现异常:

  • iOS 上 fixed 会变成 absolute,随页面滚动
  • Android 上 fixed 元素被键盘遮挡或被推到键盘上方

解决方案:放弃 fixed,改用 absolute + visualViewport

<div class="page-wrapper">
 div class="content">...</div>
 div class="bottom-bar">底部栏</div>
</div>
.page-wrapper {
  position: relative;
  width: 100%;
  min-height: 100vh;
  min-height: calc(var(--vh, 1vh) * 100);
}

.bottom-bar {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
}
// 实时更新 bottom-bar 位置
if (window.visualViewport) {
  window.visualViewport.addEventListener('resize', () => {
    const offset = window.innerHeight - window.visualViewport.height;
    document.querySelector('.bottom-bar').style.bottom = `${offset}px`;
  });
}

4. Android windowSoftInputMode 的影响

问题:在 Android WebView 或混合 App 中,键盘弹起行为受 windowSoftInputMode 控制,H5 侧无法直接干预但需要适配。

模式行为
adjustResizeActivity 大小调整,visualViewport 会变化(推荐)
adjustPan页面平移,H5 布局不变
adjustNothing不做任何调整,输入框可能被遮挡

H5 侧适配方案:无论原生配置哪种模式,都用 visualViewport 做 fallback:

function getKeyboardHeight() {
  if (window.visualViewport) {
    return window.innerHeight - window.visualViewport.height;
  }
  return 0;
}

function isKeyboardOpen() {
  return getKeyboardHeight() > 100; // 阈值
}

5. 输入框被键盘遮挡(不自动滚动)

问题:某些 Android 浏览器和 WKWebView 中,键盘弹起时输入框不会自动滚入可视区域。

解决方案:scrollIntoView + 延迟

function ensureVisible(input) {
  if (!input) return;

  // iOS 需要延迟等待键盘动画
  const delay = /iPhone|iPad|iPod/.test(navigator.userAgent) ? 300 : 100;

  setTimeout(() => {
    input.scrollIntoView({
      block: 'center',
      behavior: 'smooth'
    });
  }, delay);
}

// 绑定到所有输入框
document.querySelectorAll('input, textarea').forEach(el => {
  el.addEventListener('focus', () => ensureVisible(el));
});

6. iOS input 框获得焦点时页面缩放

问题:iOS 上input>font-size` 小于 16px 时,页面会自动缩放。

解决方案:强制设置 font-size >= 16px

input, textarea, select {
  font-size: 16px !important;
}

如果设计稿字号必须小于 16px,可以通过 transform: scale() 缩小容器来视觉欺骗:

.input-wrapper {
  transform: scale(0.875); /* 14px 视觉效果 */
  transform-origin: left center;
}

.input-wrapper input {
  font-size: 16px; /* 实际 16px,不会被缩放 */
}

7. 第三方键盘(搜狗 / 百度)导致的兼容问题

问题:第三方输入法键盘高度与系统键盘不一致,且可能多次触发 resize 事件。

解决方案:防抖 + 动态计算

function createKeyboardHandler(callback) {
  let lastHeight = 0;
  let timer = null;

  return function handler() {
    clearTimeout(timer);
    timer = setTimeout(() => {
      if (!window.visualViewport) return;

      const keyboardHeight = window.innerHeight - window.visualViewport.height;

      // 只有高度变化超过阈值才视为有效变化(过滤第三方键盘抖动)
      if (Math.abs(keyboardHeight - lastHeight) > 50) {
        lastHeight = keyboardHeight;
        callback({
          isOpen: keyboardHeight > 100,
          height: keyboardHeight
        });
      }
    }, 150); // 防抖 150ms
  };
}

// 使用
const onKeyboardChange = createKeyboardHandler(({ isOpen, height }) => {
  console.log('键盘状态:', isOpen ? '打开' : '关闭', '高度:', height);
  document.querySelector('.bottom-bar').style.transform =
    isOpen ? `translateY(-${height}px)` : 'translateY(0)';
});

window.visualViewport?.addEventListener('resize', onKeyboardChange);

8. iOS WebKit 的 interactive-widget=resizes-content 方案(现代方案)

iOS 16+ / Safari 16+ 支持 CSS interactive-widget 属性:

html {
  /* 让键盘弹出时缩小视口而非覆盖内容 */
  interactive-widget: resizes-content;
}

/* 或者 */
html {
  /* 让页面在键盘上方滚动 */
  interactive-widget: content-overlays;
}

配合 visualViewport 使用效果最佳。


9. Chat 场景 —— 输入框 + 底部栏随键盘上移

这是最常见的复杂场景,一个完整的封装方案:

<div class="chat-page">
 div class="chat-messages" id="chatMessages">
    <!-- 消息列表 -->
  </div>
 div class="chat-input-bar" id="chatInputBar">
   input type="text" id="chatInput" placeholder="输入消息..." />
   button id="sendBtn">发送</button>
  </div>
</div>
html {
  interactive-widget: resizes-content; /* iOS 16+ 优化 */
}

:root {
  --keyboard-height: 0px;
  --vh: 1vh;
}

.chat-page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: calc(var(--vh, 1vh) * 100);
  overflow: hidden;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  padding-bottom: calc(var(--keyboard-height) + 60px);
  /* 留出底部输入栏 + 键盘的空间 */
}

.chat-input-bar {
  position: fixed;
  bottom: var(--keyboard-height);
  left: 0;
  right: 0;
  display: flex;
  padding: 8px 16px;
  background: #fff;
  border-top: 1px solid #eee;
  transition: bottom 0.25s ease;
}

.chat-input-bar input {
  flex: 1;
  height: 40px;
  font-size: 16px; /* 防止 iOS 缩放 */
  border: 1px solid #ddd;
  border-radius: 20px;
  padding: 0 16px;
}
class KeyboardManager {
  constructor() {
    this.keyboardHeight = 0;
    this.isOpen = false;
    this.init();
  }

  init() {
    // 方案 1:visualViewport(现代浏览器)
    if (window.visualViewport) {
      window.visualViewport.addEventListener('resize', () => this.onViewportResize());
      window.visualViewport.addEventListener('scroll', () => this.onViewportResize());
      this.updateVH();
    }

    // 方案 2:原生交互(WebView 场景)
    this.setupNativeBridge();

    // 方案 3:focus/blur fallback
    this.setupFocusFallback();
  }

  updateVH() {
    const vh = window.visualViewport.height * 0.01;
    document.documentElement.style.setProperty('--vh', `${vh}px`);
  }

  onViewportResize() {
    this.updateVH();

    const height = window.innerHeight - window.visualViewport.height;
    const isOpen = height > 100;

    if (height !== this.keyboardHeight || isOpen !== this.isOpen) {
      this.keyboardHeight = height;
      this.isOpen = isOpen;

      document.documentElement.style.setProperty(
        '--keyboard-height',
        `${isOpen ? height : 0}px`
      );

      this.onKeyboardChange?.(isOpen, height);
    }
  }

  setupNativeBridge() {
    // 与原生 WebView 通信获取键盘高度(适用于混合 App)
    // Android: 通过 JSBridge 接收 keyboardWillShow / keyboardWillHide
    window.onKeyboardShow = (height) => {
      this.keyboardHeight = height;
      this.isOpen = true;
      document.documentElement.style.setProperty('--keyboard-height', `${height}px`);
    };

    window.onKeyboardHide = () => {
      this.keyboardHeight = 0;
      this.isOpen = false;
      document.documentElement.style.setProperty('--keyboard-height', '0px');
    };
  }

  setupFocusFallback() {
    // visualViewport 不可用时的降级方案
    document.addEventListener('focusin', (e) => {
      if (e.target.matches('input, textarea')) {
        setTimeout(() => {
          e.target.scrollIntoView({ block: 'center', behavior: 'smooth' });
        }, 300);
      }
    });

    document.addEventListener('focusout', () => {
      // iOS 键盘收起后页面回弹
      setTimeout(() => {
        window.scrollTo(0, Math.max(0, document.body.scrollTop));
      }, 100);
    });
  }
}

// 使用
const kb = new KeyboardManager();

kb.onKeyboardChange = (isOpen, height) => {
  // 键盘状态变化时的业务逻辑
  const messages = document.getElementById('chatMessages');

  if (isOpen) {
    // 键盘弹起,滚动消息列表到底部
    setTimeout(() => {
      messages.scrollTop = messages.scrollHeight;
    }, 250);
  }
};

总结:问题速查表

问题核心方案关键 API
页面上推 / 留白动态视口单位 --vhvisualViewport.height
fixed 失效改用 absolute + 动态 bottomvisualViewport
输入框被遮挡scrollIntoView + 延迟Element API
iOS 页面缩放font-size >= 16pxCSS
键盘高度获取不准防抖 + 阈值过滤visualViewport
第三方键盘抖动防抖 + 变化量阈值JS debounce
Android WebView原生桥接 + visualViewport 兜底JSBridge
iOS 16+ 优化interactive-widget CSS 属性CSS

核心思路:不依赖 window.innerHeight 的变化,而是使用 visualViewport API 获取真实可视区域,结合 CSS 变量动态更新布局,同时在旧浏览器做 focus/blur 事件的降级处理。