系列导航 :一:全景概览与架构设计 | 二:启动流程深度解析 | 三: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------状态栏与导航栏的启动入口
SystemBars 是 config_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 按键并不在这个静态布局里 ,而是由 NavigationBarView 在 onFinishInflate() 中通过 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 的限制。
4.2 NavigationBarView 核心逻辑
NavigationBarView 继承自 LinearLayout,持有 mBarSize、mVertical、mCurrentRotation 等状态字段,并在内部通过 mNavigationInflaterView(NavigationBarInflaterView,继承自 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_SYSTEM与FLAG_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_back、ic_sysbar_home、ic_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 导航栏的完整实现:
- SystemBars 是状态栏的启动入口,导航栏没有独立入口
- PhoneStatusBar 持有
mNavigationBarView,通过addNavigationBar()将其添加到TYPE_NAVIGATION_BAR窗口 - NavigationBarView 通过
NavigationBarInflaterView动态创建三个按键,并负责旋转适配 - KeyButtonView 通过
InputManager.injectInputEvent()注入按键事件 - PhoneWindowManager 拦截按键事件并执行 Home/Back/Recent 的实际逻辑
下一篇文章将分析 SystemUI 中另一个核心模块------通知系统 ,从 NotificationManagerService 到 PhoneStatusBar 的完整通知链路。
下一篇:AOSP7 SystemUI 源码解析(五):通知系统------从 NMS 到 SystemUI