27、SystemUI的输入法(IME)交互:InputMethodManager的调用,导航栏与输入法的避让逻辑
聊到输入法交互,很多同学第一反应就是「哦,就是键盘弹起来嘛」。其实没那么简单。SystemUI 和输入法之间的博弈,说白了就是一场关于屏幕空间的争夺战。键盘要显示,导航栏要显示,应用内容也要显示——这三者怎么和平共处?这就是我们今天要聊的核心。
InputMethodManager:谁在背后调度?
先说说 InputMethodManager,简称 IMM。它是 Android 框架层提供的一个系统服务,负责管理输入法和应用之间的通信。你每次点击输入框,键盘弹出来,背后都是 IMM 在干活。
我个人习惯把 IMM 理解成一个「中间人」。应用说「我要输入了」,IMM 就去通知输入法「该你上场了」。输入法显示出来后,IMM 还要负责把用户敲的字传递给应用。整个过程看起来简单,但涉及到的进程通信可不少。
举个例子,当用户点击 EditText 时,流程大概是这样的:
- 应用调用
InputMethodManager.showSoftInput() - IMM 通过 Binder 通知 SystemUI 中的输入法服务
- 输入法服务启动对应的 IME 进程
- IME 绘制键盘界面,并通过 SurfaceFlinger 合成显示
嗯,这里要注意一点:IMM 的调用是异步的。你调用 showSoftInput() 后,键盘不会立刻弹出来。它需要经过 Binder 通信、进程调度、窗口合成等一系列操作。所以如果你在代码里紧接着去测量布局高度,大概率拿不到正确值。
onClick 里调用 showSoftInput() 后马上测量根布局高度,结果拿到的还是键盘没弹起来时的值。后来加了个延迟 100ms 的 Handler 才搞定。其实更好的做法是监听 OnGlobalLayoutListener。
导航栏与输入法的避让逻辑
接下来是重头戏——避让逻辑。为什么需要避让?你想想看,键盘弹起来的时候,导航栏如果还挡在底部,用户就看不到键盘最下面那排按键了。尤其是现在很多手机用的是手势导航,导航条虽然变窄了,但依然存在。
Android 的避让机制主要分两种模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 调整大小(resize) | 应用窗口被压缩,给键盘腾空间 | 聊天、笔记等需要看到全部内容的场景 |
| 调整位置(pan) | 应用窗口整体上移,不改变大小 | 登录、搜索等只需要看到当前输入框的场景 |
这两种模式由 android:windowSoftInputMode 属性控制。但 SystemUI 层面的避让逻辑更底层——它处理的是导航栏和键盘之间的位置关系。
SystemUI 中的避让实现
在 SystemUI 代码里,避让逻辑主要在 NavigationBarView 和 InputMethodManager 的配合中完成。核心思路是:当键盘显示时,导航栏需要让出底部空间。
具体来说,SystemUI 会监听 InputMethodService 的 onShowInputRequested() 回调。一旦检测到键盘要显示,SystemUI 就会计算键盘的高度,然后调整导航栏的位置。
我记得在 Android 10 之前,这个逻辑还挺粗暴的——键盘弹起来,导航栏就直接隐藏了。用户想返回桌面?得先收起键盘。体验很糟糕。后来 Android 10 引入了手势导航,导航栏变成了一个细条,避让逻辑也做了优化:
- 三键导航:键盘弹起时,导航栏背景变透明,但按键依然可点击
- 手势导航:键盘弹起时,导航条上移,悬浮在键盘上方
你可能会问:「导航条上移多少?怎么算的?」
答案是:由 SystemUI 的 InsetsController 统一管理。它会根据当前键盘高度、导航栏高度、系统状态栏高度,计算出最终的 insets 值,然后分发给所有窗口。
核心流程:
- 输入法请求显示键盘
- SystemUI 的
InputMethodManager收到回调 InsetsController计算新的 insets- 导航栏根据 insets 调整自身位置
- 应用窗口收到 insets 变化,重新布局
避让逻辑的 SVG 流程图
下面这张图展示了键盘弹出时,SystemUI 内部的核心交互流程:
实际项目中的避让细节
说完了理论,聊聊我在实际项目中遇到的一些细节问题。
第一个问题是「键盘高度变化」。 不同输入法的高度不一样,甚至同一个输入法在不同语言模式下高度也不同。比如中文输入法有候选词栏,英文输入法就没有。SystemUI 怎么处理?
答案是:动态监听。SystemUI 会持续监听输入法的窗口高度变化,一旦发现变化,就重新计算 insets。这个逻辑在 InputMethodService 的 onWindowVisibilityChanged() 里触发。
第二个问题是「导航栏闪烁」。 键盘快速弹出收起时,导航栏可能会频繁切换状态,导致闪烁。我记得在 Android 11 上,Google 加了一个防抖机制——键盘状态变化后,会等待 100ms 的稳定期,再触发导航栏调整。
一个小技巧:如果你在开发中需要监听键盘弹出状态,不要直接监听 InputMethodManager 的返回值。更好的做法是监听 ViewTreeObserver.OnGlobalLayoutListener,通过对比根布局的可见高度变化来判断键盘状态。这个方法兼容性更好,而且不需要系统权限。
避让逻辑的代码片段
最后,给一段 SystemUI 中处理避让逻辑的核心代码(简化版):
// SystemUI 中的 NavigationBarController
public class NavigationBarController {
private void onImeInsetsChanged(Insets imeInsets) {
// 获取当前导航栏的 insets
Insets navInsets = getNavigationBarInsets();
// 计算新的导航栏位置
int newBottom = imeInsets.bottom + navInsets.bottom;
// 更新导航栏布局参数
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mNavigationBarView.getLayoutParams();
lp.bottomMargin = imeInsets.bottom;
mNavigationBarView.setLayoutParams(lp);
// 通知应用窗口更新 insets
mInsetsController.dispatchInsetsChanged();
}
}
这段代码看起来简单,但实际项目中要考虑的情况很多。比如横竖屏切换时,导航栏的位置会变;分屏模式下,键盘只影响其中一个窗口;还有折叠屏手机,展开和折叠状态下的避让逻辑完全不同。
嗯,这些细节说起来又是一大篇了。不过核心思想不变:SystemUI 作为系统窗口的管理者,必须保证输入法和导航栏之间不打架。所有的避让逻辑,最终目的都是让用户能顺畅地输入,同时不丢失对系统的控制权。