Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars

系列导航一:全景概览与架构设计 | 二:启动流程深度解析 | 三:StatusBar 状态栏 | 四:NavigationBar 导航栏(本文) | 五:通知系统 | 六:QuickSettings | 七:Keyguard 锁屏 | 八:Recents 与其他子系统

一、概述

NavigationBar(导航栏)是 Android 设备屏幕底部的系统栏,包含经典的**返回(Back)、主页(Home)、最近任务(Recent)**三大按键。在没有物理按键的设备上,导航栏是用户进行全局导航的唯一入口。

在 AOSP7 中,导航栏的核心源码位于:

复制代码
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/
├── phone/
│   ├── NavigationBarView.java           # ★ 导航栏视图(核心)
│   ├── NavigationBarTransitions.java    # 导航栏过渡动画
│   └── LightBarTransitionsController.java
├── policy/
│   ├── KeyButtonView.java              # 按键视图(policy 包)
│   └── DeadZone.java                   # 触摸死区处理
├── SystemBars.java                     # ★ SystemBars 入口
└── ...

二、SystemBars------状态栏与导航栏的启动入口

SystemBarsconfig_systemUIServiceComponents 中注册的组件,负责创建并启动状态栏。AOSP7 的 SystemBars 继承自 SystemUI,通过 SystemUIFactory 创建状态栏实例:

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/SystemBars.java

java 复制代码
public class SystemBars extends SystemUI {
    private PhoneStatusBar mStatusBar;

    @Override
    public void start() {
        mStatusBar = SystemUIFactory.getInstance().createStatusBar(mContext,
                (CommandQueue) mComponents.get(CommandQueue.class));
        mStatusBar.start();
    }
}

关键设计 :导航栏并没有独立的启动入口,它的创建发生在 PhoneStatusBar.start() 内部,由状态栏统一托管。

三、导航栏的创建(PhoneStatusBar 内)

PhoneStatusBar 继承自 BaseStatusBar,内部通过字段 mNavigationBarView 持有导航栏视图。它先用 View.inflate() 加载布局,再通过 addNavigationBar() 注册旋转监听、准备视图并以 TYPE_NAVIGATION_BAR 窗口类型添加到 WindowManager

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java

java 复制代码
public class PhoneStatusBar extends BaseStatusBar {
    // ...
    protected NavigationBarView mNavigationBarView;   // 导航栏视图

    protected void inflateNavigationBarView(Context context) {
        mNavigationBarView = (NavigationBarView) View.inflate(
                context, R.layout.navigation_bar, null);
    }

    protected void addNavigationBar() {
        mNavigationBarView.registerRotationWatcher();
        prepareNavigationBarView();
        mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());
    }

    private WindowManager.LayoutParams getNavigationBarLayoutParams() {
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,   // ★ 导航栏窗口类型
                0
                    | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
                    | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                    | WindowManager.LayoutParams.FLAG_SLIPPERY,
                PixelFormat.TRANSLUCENT);
        lp.setTitle("NavigationBar");
        lp.windowAnimations = 0;
        return lp;
    }
}

关键设计 :导航栏视图由状态栏直接持有并管理,TYPE_NAVIGATION_BAR 窗口类型确保它被固定在屏幕底部并享有与状态栏相同的系统级窗口待遇。

窗口属性对比:

属性 状态栏 导航栏
Window Type TYPE_STATUS_BAR TYPE_NAVIGATION_BAR
Gravity TOP BOTTOM
触摸处理 下拉手势 按键点击

四、NavigationBarView------导航栏视图

4.1 布局文件

AOSP7 的 navigation_bar.xml 非常简洁------它只声明了 NavigationBarView 根容器和一个 NavigationBarInflaterView。返回、主页、最近任务三个 KeyButtonView 按键并不在这个静态布局里 ,而是由 NavigationBarViewonFinishInflate() 中通过 NavigationBarInflaterView 动态创建并填充到对应方位的视图里。

源码路径frameworks/base/packages/SystemUI/res/layout/navigation_bar.xml

xml 复制代码
<com.android.systemui.statusbar.phone.NavigationBarView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/system_bar_background">

    <com.android.systemui.statusbar.phone.NavigationBarInflaterView
        android:id="@+id/navigation_inflater"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</com.android.systemui.statusbar.phone.NavigationBarView>

关键设计:静态布局只保留一个"充气器"(Inflater),三个按键由代码创建,这样导航栏才能根据设备方向动态调整按键位置与顺序,而不受静态 XML 的限制。

NavigationBarView 继承自 LinearLayout,持有 mBarSizemVerticalmCurrentRotation 等状态字段,并在内部通过 mNavigationInflaterViewNavigationBarInflaterView,继承自 FrameLayout)来创建和管理按键。onFinishInflate() 中找到充气器并注册按键分发器,reorient() 根据当前方向切换视图布局(横屏时导航栏移到屏幕侧边),并刷新死区、菜单可见性与按键样式。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java

java 复制代码
public class NavigationBarView extends LinearLayout {
    // ...
    int mBarSize;                          // 导航栏高度
    boolean mVertical;                     // 是否竖向(横屏时为 false)
    private int mCurrentRotation = -1;     // 当前屏幕方向
    private NavigationBarInflaterView mNavigationInflaterView;  // 按键充气器

    @Override
    public void onFinishInflate() {
        mNavigationInflaterView = (NavigationBarInflaterView) findViewById(
                R.id.navigation_inflater);
        updateRotatedViews();
        mNavigationInflaterView.setButtonDispatchers(mButtonDisatchers);
        // ...
    }

    public void reorient() {
        updateCurrentView();               // 按方向切换视图
        // ... 更新死区、菜单可见性、按键样式
        mBarTransitions.init();
        setNavigationIconHints(mNavigationIconHints, true);
        getHomeButton().setVertical(mVertical);
    }
}

关键设计 :导航栏把"按键创建"与"按键布局"解耦------NavigationBarView 只负责方向切换和状态同步,具体按键的创建交给 NavigationBarInflaterView,因此横竖屏下按键都能正确摆放。

五、KeyButtonView------单个按键视图

KeyButtonView 是导航栏中每个按键的具体实现。它继承自 ImageView,并实现 ButtonDispatcher.ButtonInterface 接口。构造时从 XML 属性中读出按键码 mCode 与是否支持长按 mSupportsLongpress,触摸时通过 sendEvent() 构造 KeyEvent 并注入 Input 系统。

源码路径frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java

java 复制代码
public class KeyButtonView extends ImageView implements ButtonDispatcher.ButtonInterface {
    // ...
    private int mCode;                    // 按键码(如 KEYCODE_BACK)
    private boolean mSupportsLongpress = true;  // 是否支持长按
    private long mDownTime;               // 按下时间,用作 KeyEvent 的 downTime

    public KeyButtonView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.KeyButtonView, defStyle, 0);
        mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);
        mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true);
        a.recycle();
        setClickable(true);
        setBackground(new KeyButtonRipple(context, this));
    }

    public boolean onTouchEvent(MotionEvent ev) {
        // ...
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownTime = SystemClock.uptimeMillis();
                if (mCode != 0) {
                    sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
                }
                // ...
                break;
            case MotionEvent.ACTION_UP:
                // ...
                if (mCode != 0 && isPressed()) {
                    sendEvent(KeyEvent.ACTION_UP, 0);
                }
                // ...
                break;
        }
        return true;
    }

    void sendEvent(int action, int flags, long when) {
        final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
        final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
                0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
                InputDevice.SOURCE_KEYBOARD);
        InputManager.getInstance().injectInputEvent(ev,
                InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
    }
}

关键设计sendEvent() 构造的 KeyEvent 带有 VIRTUAL_KEYBOARD 设备 id、FLAG_FROM_SYSTEMFLAG_VIRTUAL_HARD_KEY 标志,并通过 InputManager.injectInputEvent() 异步注入。这让导航栏虚拟按键在 Input 系统中与物理按键走完全相同的处理链路,行为保持一致。

六、导航栏与 PhoneWindowManager 的交互

导航栏按键经 InputManager 注入后,会进入 WindowManagerPolicy 的处理流程,由 PhoneWindowManager 拦截并执行 Home/Back/Recent 的实际逻辑。下面这段代码仅作概念示意(简化) ,用于说明拦截与分发的思路,并非 PhoneWindowManager 的真实方法体------真实的 interceptKeyBeforeDispatching() 处理大量锁屏、分屏、多窗口等边界情况,远比此处复杂:

源码路径frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java

java 复制代码
// 概念示意(简化):仅说明拦截思路,非真实方法体
public class PhoneWindowManager implements WindowManagerPolicy {
    public long interceptKeyBeforeDispatching(...) {
        // 依据 event.getKeyCode() 区分 Home / Back / APP_SWITCH
        // 对需要直接消费的事件返回非 0,否则继续向下分发
        return 0;
    }
}

关键设计 :虚拟导航栏按键与物理按键最终都汇聚到 WindowManagerPolicy 的统一拦截点,由 PhoneWindowManager 决定消费或继续分发,这正是虚拟按键能"以假乱真"的根本原因。

七、导航栏的显示与隐藏

7.1 沉浸式模式

应用可以通过 SYSTEM_UI_FLAG_HIDE_NAVIGATION 隐藏导航栏,这是应用侧的调用示例(非 SystemUI 源码):

java 复制代码
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
    View.SYSTEM_UI_FLAG_HIDE_NAVIGATION      // 隐藏导航栏
    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY   // 粘性沉浸模式
    | View.SYSTEM_UI_FLAG_FULLSCREEN         // 全屏
);

关键设计 :沉浸式标志位通过 setSystemUiVisibility() 下发到系统,由 SystemUI 侧统一换算成状态栏/导航栏的可见性。

7.2 SystemUI 侧的响应

SystemUI 侧根据应用下发的可见性标志位调整导航栏的显示/隐藏,概念上如下(简化示意,真实处理还要结合全屏与 docked 窗口的状态位):

java 复制代码
// 概念示意(简化)
boolean hideNavBar = (vis & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0;
mNavigationBarView.setVisibility(hideNavBar ? View.GONE : View.VISIBLE);

关键设计 :导航栏的显隐是响应式的------应用声明意图,PhoneStatusBar 持有 mNavigationBarView 并按标志位同步视图可见性。

八、导航栏定制指南

8.1 修改按键顺序

AOSP7 中三个按键由 NavigationBarView/NavigationBarInflaterView 动态创建,按键顺序与位置由代码(而非静态 navigation_bar.xml)决定。因此修改按键顺序需要调整按键创建与排序的逻辑,例如把最近任务键放到返回键一侧。

8.2 修改按键图标

替换 ic_sysbar_backic_sysbar_homeic_sysbar_recent 对应的 drawable 资源即可,按键图标在代码中按按键码动态加载。

8.3 添加自定义按键

由于按键是程序化创建的,添加自定义按键需要在 NavigationBarView 的按键创建逻辑中新增一个 KeyButtonView 并注册对应的 ButtonDispatcher,而不能直接在 navigation_bar.xml 里加一个静态节点。

8.4 修改导航栏高度

导航栏高度由 dimens.xml 中的尺寸资源控制,修改对应 dimen 即可(AOSP7 中 NavigationBarView 实际读取 R.dimen.navigation_bar_size):

源码路径frameworks/base/core/res/res/values/dimens.xml

xml 复制代码
<dimen name="navigation_bar_height">48dp</dimen>

关键设计:导航栏的高度来自资源文件,便于不同分辨率与形态的设备适配,无需改代码。

8.5 常见踩坑点

  • 导航栏不显示 :确认设备没有物理按键(config_showNavigationBar 为 true)
  • 按键无响应 :检查 InputManager.injectInputEvent 是否有权限(需要 INJECT_EVENTS 权限)
  • 按键事件被拦截PhoneWindowManager 中可能拦截了特定按键

九、小结

本文深入分析了 NavigationBar 导航栏的完整实现:

  1. SystemBars 是状态栏的启动入口,导航栏没有独立入口
  2. PhoneStatusBar 持有 mNavigationBarView,通过 addNavigationBar() 将其添加到 TYPE_NAVIGATION_BAR 窗口
  3. NavigationBarView 通过 NavigationBarInflaterView 动态创建三个按键,并负责旋转适配
  4. KeyButtonView 通过 InputManager.injectInputEvent() 注入按键事件
  5. PhoneWindowManager 拦截按键事件并执行 Home/Back/Recent 的实际逻辑

下一篇文章将分析 SystemUI 中另一个核心模块------通知系统 ,从 NotificationManagerServicePhoneStatusBar 的完整通知链路。


下一篇:AOSP7 SystemUI 源码解析(五):通知系统------从 NMS 到 SystemUI

相关推荐
JMchen1 小时前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 小时前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui
2501_916007471 小时前
苹果商店iOS应用上架费用全面解析:开发者计划与审核费用计算
android·ios·小程序·https·uni-app·iphone·webview
Android打工仔1 小时前
Kotlin 协程源码解析:协程是如何切换线程的?
android·kotlin
mmsx1 小时前
Android GIS系列 内存加锁、SQLite 开事务:矢量数据集双层并发设计
android·性能优化·app
Godikov1 小时前
没有 adb 怎么排查接口问题?给 OkHttp 加一个"永不炸"的文件日志拦截器
android
邪修king2 小时前
Re:Linux 系统篇(三十四):进程间通信开篇Chapter1 —— 匿名管道 pipe 深度拆解与代码实战
android·linux·运维·开发语言
AFinalStone2 小时前
Android7 SystemUI 源码解析(一)全景概览与架构设计
android·systemui
2501_916008893 小时前
如何实现iOS App代码混淆:SwiftShield使用指南
android·ios·小程序·https·uni-app·iphone·webview