SystemUI 系统导航栏 NavigationBar 分析(AndroidO)
1 概述
导航栏是指在屏幕底端或侧边容纳了一排虚拟按键的一个窗口,这些按键包括了常用的返回键、主页键、最近任务键等。导航栏窗口作为 SystemUI 的一个重要组件,它的启动时机是在状态栏之后,同时导航栏窗口也是通过 WindowManager.addView() 方法创建的。
2 导航栏分析
2.1 导航栏窗口创建
和状态栏一样,导航栏的创建入口也是在 StatusBar.makeStatusBarView()。
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java
protected void makeStatusBarView() {
// 创建状态栏...
...
// 创建导航栏
try {
boolean showNav = mWindowManagerService.hasNavigationBar(); // 1 是否显示导航栏
if (showNav) {
createNavigationBar(); // 2 创建导航栏的控件树
}
} catch (RemoteException ex) {
// no window manager? good luck with that
}
...
}
通过 hasNavigationBar() 判断是否需要创建显示导航栏,首先看配置文件,然后检查系统属性 qemu.hw.mainkeys 是否设置,两个判断标志都在 PhoneWindowManager 类的 setInitialDisplaySize() 方法中获取设置。
// frameworks\base\services\core\java\com\android\server\wm\WindowManagerService.java
public boolean hasNavigationBar() {
return mPolicy.hasNavigationBar();
}
// frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java
public class PhoneWindowManager implements WindowManagerPolicy {
boolean mHasNavigationBar = false;
public boolean hasNavigationBar() {
return mHasNavigationBar; //
}
public void setInitialDisplaySize(Display display, int width, int height, int density) {
// frameworks\base\core\res\res\values\config.xml 默认为true
mHasNavigationBar = res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
// 可以在模拟器中通过设置系统属性进行覆盖
String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
if ("1".equals(navBarOverride)) {
mHasNavigationBar = false;
} else if ("0".equals(navBarOverride)) {
mHasNavigationBar = true;
}
}
}
createNavigationBar() 通过调用 NavigationBarFragment.create() 来加载导航栏控件树。
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java
private NavigationBarFragment mNavigationBar;
private View mNavigationBarView;
protected void createNavigationBar() {
// navigation_bar_window.xml
mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {
// navigation_bar.xml
mNavigationBar = (NavigationBarFragment) fragment;
if (mLightBarController != null) {
mNavigationBar.setLightBarController(mLightBarController);
}
mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
});
}
create() 方法和创建状态栏相似,同样是调用 WindowManager.addView 创建导航栏窗口,而在 onCreateView() 方法中真正加载的 RootView 是 navigation_bar.xml 布局。在这里,导航栏的窗口类型是 TYPE_NAVIGATION_BAR,导航栏的宽度高度都是 MATCH_PARENT,但不会充满整个屏幕,而是充满由 WindowManager 提供的父布局 ,其大小是在 PhoneWindowManager.beginLayoutLw() 中计算的,后面会分析到。
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarFragment.java
public static View create(Context context, FragmentListener listener) {
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, // 高度宽度
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR, //窗口类型 FIRST_SYSTEM_WINDOW+19
WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING // 接受导致设备唤醒的触摸事件
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE //不接受按键事件
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL // 消耗所有指针事件本身,而不管它们是否在窗口内
| WindowManager.LayoutParams.FLAG_SPLIT_TOUCH // // 该窗口将接受超出其边界的触摸事件,并将其发送到也支持拆分触摸的其他窗口
| WindowManager.LayoutParams.FLAG_SLIPPERY, // 在手势中期,使触摸能够从窗口滑出到相邻窗口中,而不是在手势持续时间内被捕获
PixelFormat.TRANSLUCENT);
lp.token = new Binder();
lp.setTitle("NavigationBar");
lp.windowAnimations = 0; // 不使用窗口动画
//加载导航栏控件树 frameworks\base\packages\SystemUI\res\layout\navigation_bar_window.xml
View navigationBarView = LayoutInflater.from(context).inflate(
R.layout.navigation_bar_window, null);
if (navigationBarView == null) return null;
// 创建导航栏窗口
context.getSystemService(WindowManager.class).addView(navigationBarView, lp);
FragmentHostManager fragmentHost = FragmentHostManager.get(navigationBarView);
NavigationBarFragment fragment = new NavigationBarFragment();
fragmentHost.getFragmentManager().beginTransaction()
.replace(R.id.navigation_bar_frame, fragment, TAG) // navigation_bar_window.xml
.commit();
fragmentHost.addTagListener(TAG, listener);
return navigationBarView;
}
NavigationBarFragment
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarFragment.java
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) {
// 加载布局 frameworks\base\packages\SystemUI\res\layout\navigation_bar.xml
return inflater.inflate(R.layout.navigation_bar, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mNavigationBarView = (NavigationBarView) view;
// 设置导航栏按键触摸事件的监听器
prepareNavigationBarView();
...
}
private void prepareNavigationBarView() {
mNavigationBarView.reorient();
ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
recentsButton.setOnClickListener(this::onRecentsClick);
recentsButton.setOnTouchListener(this::onRecentsTouch);
recentsButton.setLongClickable(true);
recentsButton.setOnLongClickListener(this::onLongPressBackRecents);
ButtonDispatcher backButton = mNavigationBarView.getBackButton();
backButton.setLongClickable(true);
backButton.setOnLongClickListener(this::onLongPressBackRecents);
ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
homeButton.setOnTouchListener(this::onHomeTouch);
homeButton.setOnLongClickListener(this::onHomeLongClick);
...
}
再来看一下 PhoneWindowManager.beginLayoutLw() 是怎么计算的导航栏位置的。
一般来说,导航栏位于屏幕底部和右侧,这是根据屏幕方向及高宽大小决定的。 这里 mNavigationBarCanMove 变量代表导航栏是否可以移动,是在PhoneWindowManager.setInitialDisplaySize() 方法中设置的。mNavigationBarPosition 变量代表导航栏最终的布局位置。
// frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java
public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight,int displayRotation, int uiMode) {
...
boolean updateSysUiVisibility = layoutNavigationBar(displayWidth, displayHeight,displayRotation, uiMode, overscanLeft, overscanRight, overscanBottom, dcf, navVisible, navTranslucent,navAllowedHidden, statusBarExpandedNotKeyguard); //
updateSysUiVisibility |= layoutStatusBar(pf, df, of, vf, dcf, sysui, isKeyguardShowing); //
if (updateSysUiVisibility) {
updateSystemUiVisibilityLw();
}
}
private boolean<