16、导航栏(NavigationBar)深度解析:三键导航与手势导航的切换逻辑,KeyEvent的分发

导航栏这东西,说白了就是用户和系统交互的“最后一公里”。我做了这么多年Android框架,每次看到导航栏的代码都忍不住感慨——这玩意儿看着简单,里面的坑是真不少。今天咱们就把它彻底扒开看看。

16.1 三键导航 vs 手势导航:两种模式的本质

先问个问题:为什么Android要搞两套导航方案?

我个人习惯把这个问题拆成两个层面来看:

  • 用户层面:三键导航是“点按”思维,手势导航是“滑动”思维
  • 系统层面:三键导航靠KeyEvent,手势导航靠TouchEvent

嗯,这里要注意,这两种模式在SystemUI里是互斥的。你不可能同时启用两套方案,否则用户会疯掉。

核心结论:三键导航和手势导航的切换,本质上是SystemUI中NavigationBarFragment的替换过程。

16.2 切换逻辑:从Settings到SystemUI的完整链路

我记得第一次看这个流程时,差点被绕晕。咱们一步步拆解:

  1. 用户设置:在Settings里选择“手势导航”或“三键导航”
  2. Settings写入:将选择写入Settings.System的navigation_mode字段
  3. SystemUI监听:NavigationModeController监听这个字段的变化
  4. Fragment替换:NavigationBarFragment根据模式加载不同的布局

来看关键代码:

// NavigationModeController.java
public int getNavigationMode(Context context) {
    int mode = Settings.System.getInt(
        context.getContentResolver(),
        Settings.System.NAVIGATION_MODE,
        NAVIGATION_MODE_GESTURAL  // 默认手势导航
    );
    return mode;
}

避坑指南:我曾经在项目里遇到一个问题——切换导航模式后,导航栏没有立即刷新。后来发现是ContentObserver没有正确注册。记住,一定要在onAttach里注册监听,在onDetach里取消。

16.3 三键导航的KeyEvent分发机制

三键导航的核心就是三个虚拟按键:Back、Home、Recent。每个按键对应一个KeyEvent。

分发流程是这样的:

  1. 用户点击导航栏上的某个按键
  2. NavigationBarView捕获到点击事件
  3. 通过KeyEvent将事件发送到WindowManagerService
  4. WMS根据当前焦点窗口决定如何处理

举个例子,Back键的分发:

// NavigationBarView.java
public boolean onTouchEvent(MotionEvent event) {
    // 检测到Back键被点击
    if (isBackKeyTouched(event)) {
        // 构造KeyEvent
        KeyEvent keyEvent = new KeyEvent(
            KeyEvent.ACTION_DOWN, 
            KeyEvent.KEYCODE_BACK
        );
        // 发送到WMS
        mWindowManager.injectInputEvent(keyEvent, 
            InputDevice.SOURCE_CLASS_BUTTON);
    }
    return super.onTouchEvent(event);
}

注意:这里有个细节——三键导航的KeyEvent是注入到系统输入管道里的,而不是直接调用某个方法。这样做的好处是,所有窗口都能公平地接收到事件,包括第三方应用。

16.4 手势导航的TouchEvent处理

手势导航就完全不一样了。它不产生KeyEvent,而是直接处理TouchEvent。

我画了一张图,帮你理解手势导航的核心逻辑:

手势导航核心逻辑 手势检测层 手势识别层 动作执行层 监听屏幕底部区域 的Touch事件 判断滑动方向 (上滑、左滑、右滑) 执行对应操作 (返回、主页、多任务) 关键区别 三键导航:KeyEvent → WMS → 窗口 手势导航:TouchEvent → GestureDetector → 动作

手势导航的核心类在SystemUI里叫GestureNavigationView。它内部维护了一个GestureDetector,用来识别各种手势:

// GestureNavigationView.java
public boolean onTouchEvent(MotionEvent event) {
    // 交给GestureDetector处理
    mGestureDetector.onTouchEvent(event);
    return true;  // 消费事件,不让下层处理
}

private class GestureListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onSwipeUp(MotionEvent e1, MotionEvent e2) {
        // 上滑 → 回到主页
        performHomeAction();
        return true;
    }
    
    @Override
    public boolean onSwipeLeft(MotionEvent e1, MotionEvent e2) {
        // 左滑 → 返回
        performBackAction();
        return true;
    }
}

关键点:手势导航的TouchEvent是被消费的。一旦手势被识别,事件就不会再往下传递。这就是为什么你在手势导航模式下,无法在屏幕底部区域触发其他触摸事件。

16.5 两种模式的共存与切换

你可能会问:系统怎么知道当前该用哪种模式?

答案在NavigationBarFragment里。这个Fragment会根据NavigationModeController返回的模式,动态加载不同的布局:

// NavigationBarFragment.java
@Override
public View onCreateView(LayoutInflater inflater, 
                         ViewGroup container, 
                         Bundle savedInstanceState) {
    int mode = mNavigationModeController.getNavigationMode(getContext());
    
    if (mode == NAVIGATION_MODE_GESTURAL) {
        // 加载手势导航布局
        return inflater.inflate(
            R.layout.navigation_bar_gestural, container, false);
    } else {
        // 加载三键导航布局
        return inflater.inflate(
            R.layout.navigation_bar_3button, container, false);
    }
}

个人经验:我在做定制ROM时,遇到过一个问题——切换模式后,导航栏的高度没有自适应。三键导航需要更高的导航栏(因为要放三个按钮),而手势导航只需要一个窄条。后来发现是NavigationBarFrameonMeasure没有根据模式动态调整高度。解决方案是在onCreateView之后,重新请求布局。

16.6 KeyEvent分发的完整链路

最后,咱们把KeyEvent的分发链路串起来。不管你是三键导航还是手势导航,最终都会落到这个链路上:

层级 组件 职责
1 InputReader 从硬件读取输入事件
2 InputDispatcher 分发事件到目标窗口
3 WindowManagerService 确定焦点窗口,处理系统按键
4 PhoneWindow 将KeyEvent分发给Activity
5 Activity/View 处理或消费事件

这里有个细节:系统按键(比如Back键)在WMS层就会被拦截,不会直接发给Activity。除非Activity通过onKeyDowndispatchKeyEvent主动处理。

避坑指南:我曾经遇到一个bug——某个应用在后台播放视频时,用户按Back键无法退出。排查后发现,是应用在onKeyDown里消费了Back事件,但没有调用super.onKeyDown。记住,如果你要拦截Back键,一定要确保在合适的时机调用super方法,否则系统无法执行默认的返回操作。

好了,导航栏的深度解析就到这里。三键导航和手势导航的切换逻辑,说白了就是Fragment的替换 + 输入事件类型的切换。KeyEvent的分发链路虽然长,但只要抓住WMS这个核心节点,就不会迷路。


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