27、SystemUI的输入法(IME)交互:InputMethodManager的调用,导航栏与输入法的避让逻辑

聊到输入法交互,很多同学第一反应就是「哦,就是键盘弹起来嘛」。其实没那么简单。SystemUI 和输入法之间的博弈,说白了就是一场关于屏幕空间的争夺战。键盘要显示,导航栏要显示,应用内容也要显示——这三者怎么和平共处?这就是我们今天要聊的核心。

InputMethodManager:谁在背后调度?

先说说 InputMethodManager,简称 IMM。它是 Android 框架层提供的一个系统服务,负责管理输入法和应用之间的通信。你每次点击输入框,键盘弹出来,背后都是 IMM 在干活。

我个人习惯把 IMM 理解成一个「中间人」。应用说「我要输入了」,IMM 就去通知输入法「该你上场了」。输入法显示出来后,IMM 还要负责把用户敲的字传递给应用。整个过程看起来简单,但涉及到的进程通信可不少。

举个例子,当用户点击 EditText 时,流程大概是这样的:

  1. 应用调用 InputMethodManager.showSoftInput()
  2. IMM 通过 Binder 通知 SystemUI 中的输入法服务
  3. 输入法服务启动对应的 IME 进程
  4. IME 绘制键盘界面,并通过 SurfaceFlinger 合成显示

嗯,这里要注意一点:IMM 的调用是异步的。你调用 showSoftInput() 后,键盘不会立刻弹出来。它需要经过 Binder 通信、进程调度、窗口合成等一系列操作。所以如果你在代码里紧接着去测量布局高度,大概率拿不到正确值。

我曾经踩过这个坑:在某个项目中,我需要监听键盘弹出事件来做布局适配。我直接在 onClick 里调用 showSoftInput() 后马上测量根布局高度,结果拿到的还是键盘没弹起来时的值。后来加了个延迟 100ms 的 Handler 才搞定。其实更好的做法是监听 OnGlobalLayoutListener

导航栏与输入法的避让逻辑

接下来是重头戏——避让逻辑。为什么需要避让?你想想看,键盘弹起来的时候,导航栏如果还挡在底部,用户就看不到键盘最下面那排按键了。尤其是现在很多手机用的是手势导航,导航条虽然变窄了,但依然存在。

Android 的避让机制主要分两种模式:

模式 说明 适用场景
调整大小(resize) 应用窗口被压缩,给键盘腾空间 聊天、笔记等需要看到全部内容的场景
调整位置(pan) 应用窗口整体上移,不改变大小 登录、搜索等只需要看到当前输入框的场景

这两种模式由 android:windowSoftInputMode 属性控制。但 SystemUI 层面的避让逻辑更底层——它处理的是导航栏和键盘之间的位置关系。

SystemUI 中的避让实现

在 SystemUI 代码里,避让逻辑主要在 NavigationBarViewInputMethodManager 的配合中完成。核心思路是:当键盘显示时,导航栏需要让出底部空间

具体来说,SystemUI 会监听 InputMethodServiceonShowInputRequested() 回调。一旦检测到键盘要显示,SystemUI 就会计算键盘的高度,然后调整导航栏的位置。

我记得在 Android 10 之前,这个逻辑还挺粗暴的——键盘弹起来,导航栏就直接隐藏了。用户想返回桌面?得先收起键盘。体验很糟糕。后来 Android 10 引入了手势导航,导航栏变成了一个细条,避让逻辑也做了优化:

  • 三键导航:键盘弹起时,导航栏背景变透明,但按键依然可点击
  • 手势导航:键盘弹起时,导航条上移,悬浮在键盘上方

你可能会问:「导航条上移多少?怎么算的?」

答案是:由 SystemUI 的 InsetsController 统一管理。它会根据当前键盘高度、导航栏高度、系统状态栏高度,计算出最终的 insets 值,然后分发给所有窗口。

核心流程:

  1. 输入法请求显示键盘
  2. SystemUI 的 InputMethodManager 收到回调
  3. InsetsController 计算新的 insets
  4. 导航栏根据 insets 调整自身位置
  5. 应用窗口收到 insets 变化,重新布局

避让逻辑的 SVG 流程图

下面这张图展示了键盘弹出时,SystemUI 内部的核心交互流程:

SystemUI 输入法避让逻辑流程图 应用 InputMethodManager SystemUI showSoftInput() Binder 回调 InsetsController 计算 insets 导航栏 调整位置 输入法(IME) 通知显示 输入事件 实线:主流程 | 虚线:数据回传

实际项目中的避让细节

说完了理论,聊聊我在实际项目中遇到的一些细节问题。

第一个问题是「键盘高度变化」。 不同输入法的高度不一样,甚至同一个输入法在不同语言模式下高度也不同。比如中文输入法有候选词栏,英文输入法就没有。SystemUI 怎么处理?

答案是:动态监听。SystemUI 会持续监听输入法的窗口高度变化,一旦发现变化,就重新计算 insets。这个逻辑在 InputMethodServiceonWindowVisibilityChanged() 里触发。

第二个问题是「导航栏闪烁」。 键盘快速弹出收起时,导航栏可能会频繁切换状态,导致闪烁。我记得在 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 作为系统窗口的管理者,必须保证输入法和导航栏之间不打架。所有的避让逻辑,最终目的都是让用户能顺畅地输入,同时不丢失对系统的控制权。


公众号:蓝海资料掘金营,微信deep3321