> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/frontend/19-shi-kou-he-ruan-jian-pan-dui-shi-kou-de-ying-xiang.md).

# 视口和软键盘对视口的影响

视口（Viewport）是移动 Web 开发中一个非常重要的概念，最早是由苹果公司在推出 iPhone 手机时发明的，其目的是为了让 iPhone 的小屏幕尽可能完整显示整个网页。通过设置视口，不管网页原始的分辨率尺寸有多大，都能将其缩小显示在手机浏览器上，这样保证网页在手机上看起来更像在桌面浏览器中的样子。在苹果公司引入视口的概念后，大多数的移动开发者也都认同了这个做法。

视口（Viewport）简单来说就是**浏览器显示页面内容的屏幕区域**。但在移动端浏览器涉及三个视口（Viewport）概念 ：布局视口（Layout Viewport）、视觉视口（Visual Viewport）和理想视口（Ideal Viewport）。

![三种视口](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/three_viewports.jpg)

## 布局视口、视觉视口、理想视口的区别

#### 布局视口（Layout Viewport）

布局视口（Layout Viewport）是指**浏览器在其中绘制网页的视口，它涵盖页面上的所有元素**。布局视口的宽度取决于文档内容，不会随用户在屏幕上缩放页面而变化。

获取布局视口：

```
document.documentElement.clientWidth | document.body.clientWidth
```

**注意：** 一般移动端浏览器都默认设置了布局视口的宽度。根据设备的不同，布局视口的默认宽度有可能是 768px、980px 或 1024px等，这个宽度并不适合在手机屏幕中展示。移动端浏览器之所以采用这样的默认设置，是为了解决早期的 PC 端页面在手机上显示的问题。

#### 视觉视口（Visual Viewport）

视觉视口（Visual Viewport）是指是屏幕上实际可见的内容，不包括屏幕键盘、缩放区域之外的区域或任何其他不随页面尺寸缩放的屏幕控件。视觉视口区域的宽度是指浏览器窗口的宽度，而移动设备的浏览器窗口宽度一般也是整个屏幕的宽度。

获取视觉视口：

```
window.innerWidth
```

**注意：** 当用户缩放页面，或者屏幕键盘等用户界面功能开启/关闭时，视觉视口会变化，但布局视口不会改变。

#### 理想视口（Ideal Viewport）

理想视口（Ideal Viewport）是指对设备来讲最理想的视口尺寸，**即布局视口和屏幕宽度一致**。采用理想视口的方式，可以使网页在移动端浏览器上获得最理想的浏览和阅读的宽度。

设置理想视口：

```
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no">
```

获取理想视口：

```
window.screen.width
```

## 软键盘显示模式

软键盘的本质是什么？软键盘其实是一个 Dialog。

InputMethodService 为输入法创建了一个 Dialog，并且将该 Dialog 的 某些参数进行了设置，使之能够在底部或者全屏显示。当点击输入框时，系统对活动主窗口进行调整，从而为输入法腾出相应的空间，然后将该 Dialog 显示在底部，或者全屏显示。

Android 定义了一个属性，名字为 windowSoftInputMode，这个属性用于设置 Activity 主窗口与软键盘的交互模式，用于避免软键盘遮挡内容的问题。可以在 AndroidManifet.xml 中对 Activity 进行设置。如：android:windowSoftInputMode=”stateUnchanged|adjustPan”。该属性可选的值有两部分：一部分为软键盘的状态控制，控制软键盘是隐藏还是显示；另一部分是 Activity 窗口的调整，以便腾出空间展示软键盘。

android:windowSoftInputMode 的属性设置必须是下面中的一个值，或一个 ”state” 值加一个 ”adjust” 值的组合，各个值之间用 | 分开：

* **stateUnspecified：未指定状态。** 软件默认采用的就是这种交互方式，系统会根据界面采取相应的软键盘的显示模式。
* **stateUnchanged：不改变状态。** 当前界面的软键盘状态，取决于上一个界面的软键盘状态，无论是隐藏还是显示。
* **stateHidden：隐藏状态。** 软键盘总是被隐藏，不管是否有输入的需求。
* **stateAlwaysHidden：总是隐藏状态。** 和 stateHidden 不同的是，当跳转到下个界面，如果下个页面的软键盘是显示的，而我们再次回来的时候，软键盘就会隐藏起来。
* **stateVisible：可见状态。** 软键盘总是可见的，即使在界面上没有输入框的情况下也可以强制弹出来。
* **stateAlwaysVisible：总是显示状态。** 和 stateVisible 不同的是，当我们跳转到下个界面，如果下个页面软键盘是隐藏的，而我们再次回来的时候，软键盘就会显示出来。
* **adjustUnspecified：未指定模式。** 设置软键盘与软件的显示内容之间的显示关系。这个选项也是默认的设置模式。在这中情况下，系统会根据界面选择不同的模式。
* **adjustResize：调整模式。** 该模式下，窗口总是调整屏幕的大小用以保证软键盘的显示空间。
* **adjustPan：默认模式。** 该模式下，不会调整屏幕大小来保证软键盘的空间，而是采取了另外一种策略，系统会通过布局的移动，来保证用户要进行输入的输入框肯定在用户的视野范围里面，从而让用户可以看到自己输入的内容。

### iOS 的软键盘模式

iOS 默认使用 adjustPan 模式。键盘弹出时，visual viewport 尺寸有变化，但 layout viewport 尺寸不变，部分**顶部的元素**可能被遮挡或挤出可视区域。

在这个自动调整中，layout viewport 尺寸未变，但可能引发 doucment、layout viewport、visual viewport 两两间相对位置的变化。如果 viewport meta 中未指定 user-scalable=no，还会引发 zoom。另外实测显式，软键盘收起时，（由于软键盘弹出）引发的 zoom、滚动变化并不会还原，而仅仅是 visual viewport 尺寸还原。

### Android 的软键盘模式

Andriod 默认使用 adjustResize 模式（受版本或其他因素影响，也有可能未指定模式）。键盘弹出时，layout viewport 与 visual viewport 同步缩小。

该模式仍可能引发 document 滚动以便使 focus input 出现在「可见且显眼」的地方。

## 视口尺寸变更

visual viewport 在如下场景中会变更尺寸：

* 用户缩放及移动（pan）视图。
* 设备 orientation 变更。
* 浏览器工具栏、状态栏等自动隐藏/出现。
* 设备软键盘弹出/隐藏。

**注意：** visual viewport 尺寸变更时会触发 visualViewport resize 事件。

layout viewport 在页面初始渲染确定大小后，仅在如下场景下可能改变：

* 设备 orientation 变更。
* 浏览器工具栏、状态栏等自动隐藏/出现。
* 设备软键盘弹出/隐藏。

其中前两个场景，可以视作 visual viewport 尺寸变更后，浏览器为了适配「在缩放比1.0，左上角完全贴合时，layout viewport 能与 visual viewport 完全重合」而做的「让展现符合布局预期」的工作。而软键盘弹出场景，调整策略却依不同浏览器 / WebView 有不同行为。

**注意：** iOS WebView 中只有 layout viewport 尺寸变化才触发 window resize 事件。

## 检测软键盘弹出和收起

Web 并无标准 API 检测软键盘是否弹出。除非原生 WebView 增加 bridge API。

目前可靠且兼容性较好的方式是 iOS 中监听 visualViewport resize 事件，而（不支持 visualViewport API 的）Android 中监听 window 的 resize 事件（必需指定相应的软键盘显示模式，否则 viewport 尺寸不会变化）。

**注意：** 在苹果手机上，当软键盘收起时输入框会失去焦点。因此，也可以在苹果手机上可以通过输入框失去和得到焦点事件来判断。

**注意：** Android 软键盘快速工具栏右侧的「收起」按钮，点击后会收起软键盘，但 input 标签却并不失焦，此时 blur 事件未触发（不能以此为据获知软键盘收起）。

```javascript
const ua = window.navigator.userAgent;
const isIOS = /iPhone|iPod|iPad/i.test(ua)
const isAndroid = /Android/i.test(ua)

// iOS端：需要排查 input、textarea 之外的标签（如：radio、checkbox）触发 focusin、focusout
window.addEventListener('focusin', () => {
	console.log('键盘弹出')
});
window.addEventListener('focusout', () => {
	console.log('键盘收起')
})

// Android端：横屏功能也会触发 resize（可通过宽度是否改变判断）
const innerHeight = window.innerHeight
window.addEventListener('resize', () => {
  const newInnerHeight = window.innerHeight;
  if (innerHeight > newInnerHeight) {
		console.log('键盘弹出');     
  } else {
		console.log('键盘收起')；
  }
})
```

## 相关问题

### 视口单位（Viewport units）

**视口：** PC 端指的是浏览器的可视区域；移动端指的就是 Viewport 中的 Layout Viewport。

根据 CSS3 规范，视口单位主要包括以下4个：

* vw：1vw 等于视口宽度的1%。
* vh：1vh 等于视口高度的1%。
* vmin：选取 vw 和 vh 中最小的那个。
* vmax：选取 vw 和 vh 中最大的那个。

vw 和 vh 是相对于视口的高度和宽度，而不是父元素的（CSS 百分比是相对于包含它的最近的父元素的高度和宽度）。1vh 等于1/100的视口高度，1vw 等于1/100的视口宽度。

vmax 相对于视口的宽度或高度中较大的那个，其中最大的那个被均分为 100 单位的 vmax。

vmin 相对于视口的宽度或高度中较小的那个，其中最小的那个被均分为 100 单位的 vmin。

### IOS 机型软键盘遮挡问题

**场景：** van-popup + fixed 弹层，弹层内的输入框，唤起软键盘时，有概率会悬浮在 popup 上，即遮挡弹层内的内容（正常现象是，软键盘唤起，输入框自动滚动到可视区域）。

**问题排查：** 跟 css 的 user-select 的属性有关，表现为：

* 默认值 auto，容易触发遮挡情况，值改为 none 后，未发现遮挡情况。
* 默认值 auto，input 的 placeholder 属性有值，容易出现遮挡情况；placeholder 不设置或设置为空，未发现遮挡情况。

\*\*原因：\*\*这是 iOS 上常见的 `position: fixed` 弹窗在软键盘弹出时的布局问题，可能原因：

* 选择行为与键盘响应的耦合：点击输入框会出现文本选择菜单（当 user-select: auto/text）、放大镜光标控制（移动光标位置），这些交互可能与键盘弹出逻辑存在隐含的关联。
* iOS Safari 的 "滚动优化" 机制：在检测到可选择的文本输入时，会采用不同的滚动策略（自动滚动、标准键盘处理）。

**解决**：设置 `user-select: none;`。

## 参考资料

[jianshu - 关于Android中的软键盘](https://www.jianshu.com/p/50c35e1bf09b)

[viewport 与软键盘对布局的影响](https://git.winbaoxian.com/wy-front/fed-knowledge-share/issues/59#tldr)
