03. SystemUI 系统导航栏 NavigationBar 分析(AndroidO)
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 layoutNavigationBar(int displayWidth, int displayHeight, int displayRotation,
int uiMode, int overscanLeft, int overscanRight, int overscanBottom, ...) {
if (mNavigationBar != null) {
// 根据屏幕方向及高宽大小选择导航栏的位置
mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight,displayRotation); //
if (mNavigationBarPosition == NAV_BAR_BOTTOM) { // 在屏幕底部
int top = displayHeight - overscanBottom
- getNavigationBarHeight(displayRotation, uiMode);
mTmpNavigationFrame.set(0, top, displayWidth, displayHeight - overscanBottom);
...
} else if (mNavigationBarPosition == NAV_BAR_RIGHT) { // 屏幕右侧
int left = displayWidth - overscanRight
- getNavigationBarWidth(displayRotation, uiMode);
mTmpNavigationFrame.set(left, 0, displayWidth - overscanRight, displayHeight);
...
} else if (mNavigationBarPosition == NAV_BAR_LEFT) { // 屏幕左侧
// Seascape screen; nav bar goes to the left.
int right = overscanLeft + getNavigationBarWidth(displayRotation, uiMode);
mTmpNavigationFrame.set(overscanLeft, 0, right, displayHeight);
...
}
}
...
// 计算最终的导航栏布局
mNavigationBar.computeFrameLw(mTmpNavigationFrame, mTmpNavigationFrame,mTmpNavigationFrame,
mTmpNavigationFrame, mTmpNavigationFrame, dcf,mTmpNavigationFrame, mTmpNavigationFrame);
}
private int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
// mNavigationBarCanMove 是根据 mNavigationBarCanMove = width != height && shortSizeDp < 600; 得到的
if (mNavigationBarCanMove && displayWidth > displayHeight) {
if (displayRotation == Surface.ROTATION_270) {
return NAV_BAR_LEFT;
} else {
return NAV_BAR_RIGHT;
}
}
return NAV_BAR_BOTTOM;
}
2.2 导航栏布局加载
从上面可以看到导航栏窗口布局主要加载了 navigation_bar.xml,该布局下有两个控件,一个是 NavigationBarView,一个是 NavigationBarInflaterView。
// frameworks\base\packages\SystemUI\res\layout\navigation_bar.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_height="match_parent"
android:layout_width="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>
2.2.1 NavigationBarView
在构造方法中初始化导航栏相关图标的信息。
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarView.java
public NavigationBarView(Context context, AttributeSet attrs) {
super(context, attrs);
...
mConfiguration = new Configuration();
mConfiguration.updateFrom(context.getResources().getConfiguration());
// 根据配置信息更新图标
updateIcons(context, Configuration.EMPTY, mConfiguration);
mBarTransitions = new NavigationBarTransitions(this);
// 存储导航栏相关按键信息
mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back));
mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home));
mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps));
mButtonDispatchers.put(R.id.menu, new ButtonDispatcher(R.id.menu));
mButtonDispatchers.put(R.id.ime_switcher, new ButtonDispatcher(R.id.ime_switcher));
mButtonDispatchers.put(R.id.accessibility_button, new ButtonDispatcher(R.id.accessibility_button));
}
updateIcons() 根据不同的情况会重新加载导航栏图标,除了在构造方法中,这个方法也会在 onConfigurationChanged() 中被调用
private void updateIcons(Context ctx, Configuration oldConfig, Configuration newConfig) {
if (oldConfig.orientation != newConfig.orientation
|| oldConfig.densityDpi != newConfig.densityDpi) {
// 更新分屏后的最近任务图标
mDockedIcon = getDrawable(ctx,
R.drawable.ic_sysbar_docked, R.drawable.ic_sysbar_docked_dark);
}
if (oldConfig.densityDpi != newConfig.densityDpi) {
mBackIcon = getDrawable(ctx, R.drawable.ic_sysbar_back, R.drawable.ic_sysbar_back_dark);
mBackLandIcon = mBackIcon;
mBackAltIcon = getDrawable(ctx,
R.drawable.ic_sysbar_back_ime, R.drawable.ic_sysbar_back_ime_dark);
mBackAltLandIcon = mBackAltIcon;
mHomeDefaultIcon = getDrawable(ctx,
R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home_dark);
mRecentIcon = getDrawable(ctx,
R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark);
mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu, R.drawable.ic_sysbar_menu_dark);
...
}
}
再来看一下最终设置导航栏图标的方法,入口是 setNavigationIconHints(),最后会从 mButtonDispatchers 中获取不同图标布局id对应的 ButtonDispatcher,通过 ButtonDispatcher 设置图标。
public void setNavigationIconHints(int hints, boolean force) {
...
// 返回按钮图标 (在横竖屏切换时返回图标的方向也会改变)
KeyButtonDrawable backIcon = (backAlt)
? getBackIconWithAlt(mUseCarModeUi, mVertical)
: getBackIcon(mUseCarModeUi, mVertical);
getBackButton().setImageDrawable(backIcon);
// 最近任务图标 分屏情况下会显示不同的图标
// 这个方法也会被这两个方法回调: onFinishInflate() / onConfigurationChanged()
updateRecentsIcon();
if (mUseCarModeUi) {
getHomeButton().setImageDrawable(mHomeCarModeIcon);
} else {
getHomeButton().setImageDrawable(mHomeDefaultIcon); // Home 图标
}
...
}
public ButtonDispatcher getHomeButton() {
return mButtonDispatchers.get(R.id.home);
}
...
讲到这里,可能对 ButtonDispatcher 这个类会有疑惑,它到底是用来干嘛的呢?它其实是用来存储不同的虚拟按键的相关信息,但由于导航栏布局位置的差异,就会存在加载多个导航栏布局文件的情况,而按键图标,按键事件等信息自然就会对应地设置很多次,为了对这些不同布局下的同一类按键信息进行统一管理,就有了 ButtonDispatcher 。
在 NavigationBarView 内部使用了 ButtonDispatcher 类型的 Map 数组变量 mButtonDispatchers, 根据控件id来对应一个按键类型的 ButtonDispatcher,而 ButtonDispatcher 下面则又包含java了不同布局下的同一类按键信息,这里使用 View 类型数组变量 mViews 进行存储,当需要对这一类按键信息进行处理时,就会循环 mViews 数组进行设置,如更新图标、设置监听器等。
相关的代码调用链如下:
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarView.java
public void onFinishInflate() { // 在 xml 布局加载完之后,会回调 onFinishInflate() 方法
mNavigationInflaterView = (NavigationBarInflaterView) findViewById(
R.id.navigation_inflater);
updateRotatedViews();
mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
...
}
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarInflaterView.java
public void setButtonDispatchers(SparseArray<ButtonDispatcher> buttonDisatchers) {
mButtonDispatchers = buttonDisatchers;
for (int i = 0; i < buttonDisatchers.size(); i++) {
initiallyFill(buttonDisatchers.valueAt(i));
}
}
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarInflaterView.java
private void initiallyFill(ButtonDispatcher buttonDispatcher) { // 添加各个布局下的按键
addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.ends_group));
addAll(buttonDispatcher, (ViewGroup) mRot0.findViewById(R.id.center_group));
addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.ends_group));
addAll(buttonDispatcher, (ViewGroup) mRot90.findViewById(R.id.center_group));
}
private void addAll(ButtonDispatcher buttonDispatcher, ViewGroup parent) {
for (int i = 0; i < parent.getChildCount(); i++) {
// 判断parent布局下的子id 是否和当前的 buttonDispatcher 的 id 相同
if (parent.getChildAt(i).getId() == buttonDispatcher.getId()) {
buttonDispatcher.addView(parent.getChildAt(i)); // 直接添加到 mViews
} else if (parent.getChildAt(i) instanceof ViewGroup) {
addAll(buttonDispatcher, (ViewGroup) parent.getChildAt(i));
}
}
}
public class ButtonDispatcher {
private final ArrayList<View> mViews = new ArrayList<>();
// mViews 元素添加方法
void addView(View view) {
mViews.add(view);
view.setOnClickListener(mClickListener);
view.setOnTouchListener(mTouchListener);
view.setOnLongClickListener(mLongClickListener);
...
}
// 为同一类按键设置图标的方法
public void setImageDrawable(KeyButtonDrawable drawable) {
mImageDrawable = drawable;
final int N = mViews.size();
for (int i = 0; i < N; i++) {
((ButtonInterface) mViews.get(i)).setImageDrawable(mImageDrawable);
}
}
}
2.2.2 NavigationBarInflaterView
先看构造方法。
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarInflaterView.java
protected LayoutInflater mLayoutInflater;
protected LayoutInflater mLandscapeInflater;
public NavigationBarInflaterView(Context context, AttributeSet attrs) {
super(context, attrs);
Display display = ((WindowManager);
createInflaters(); //
Display display = ((WindowManager)
context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
Mode displayMode = display.getMode();
isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight(); // 是否横屏
}
createInflaters() 分别初始化了 mLayoutInflater 和 mLandscapeInflater。
private void createInflaters() {
mLayoutInflater = LayoutInflater.from(mContext);
Configuration landscape = new Configuration();
landscape.setTo(mContext.getResources().getConfiguration());
landscape.orientation = Configuration.ORIENTATION_LANDSCAPE;
mLandscapeInflater = LayoutInflater.from(mContext.createConfigurationContext(landscape));
}
在 xml 布局加载完之后,会回调 onFinishInflate() 方法
@Override
protected void onFinishInflate() {
super.onFinishInflate();
inflateChildren(); // 1 加载导航栏布局
inflateLayout(getDefaultLayout()); // 2 加载按键控件
}
protected String getDefaultLayout() {
//left,back;home;recent,right
return mContext.getString(R.string.config_navBarLayout); // 从SystemUI\res\values\config.xml配置文件中获取
}
- 加载导航栏布局
inflateChildren() 得到 mRot0 和 mRot90 两个布局,分别用于底部与侧面的导航栏布局,根据方向进行显示。
private void inflateChildren() {
removeAllViews();
mRot0 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout, this, false);
mRot0.setId(R.id.rot0);
addView(mRot0);
mRot90 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_rot90, this,
false);
mRot90.setId(R.id.rot90);
addView(mRot90);
if (getParent() instanceof NavigationBarView) {
((NavigationBarView) getParent()).updateRotatedViews(); // 更新视图
}
}
初始化不同方向的导航栏布局并根据当前方向显示对应导航栏。
void updateRotatedViews() {
mRotatedViews[Surface.ROTATION_0] =
mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0);
mRotatedViews[Surface.ROTATION_270] =
mRotatedViews[Surface.ROTATION_90] = findViewById(R.id.rot90);
final int rot = mDisplay.getRotation();
for (int i=0; i<4; i++) {
mRotatedViews[i].setVisibility(View.GONE);
}
mCurrentView = mRotatedViews[rot];
mCurrentView.setVisibility(View.VISIBLE); // 设置当前方向位置的导航栏布局可见
mNavigationInflaterView.setAlternativeOrder(rot == Surface.ROTATION_90);
for (int i = 0; i < mButtonDispatchers.size(); i++) {
mButtonDispatchers.valueAt(i).setCurrentView(mCurrentView);
}
updateLayoutTransitionsEnabled();
mCurrentRotation = rot;
}
- 加载按键控件。
先从配置文件中获取布局字符串,用于分割, 如 “left,back;home;recent,right ”,再根据字符串来进行按键控件的创建。
protected void inflateLayout(String newLayout) {
mCurrentLayout = newLayout;
if (newLayout == null) {
// left,back;home;recent,right
newLayout = getDefaultLayout();
}
String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
String[] start = sets[0].split(BUTTON_SEPARATOR);
String[] center = sets[1].split(BUTTON_SEPARATOR);
String[] end = sets[2].split(BUTTON_SEPARATOR);
// 创建按键
inflateButtons(start, (ViewGroup) mRot0.findViewById(R.id.ends_group), isRot0Landscape);
inflateButtons(start, (ViewGroup) mRot90.findViewById(R.id.ends_group), !isRot0Landscape);
inflateButtons(center, (ViewGroup) mRot0.findViewById(R.id.center_group), isRot0Landscape);
inflateButtons(center, (ViewGroup) mRot90.findViewById(R.id.center_group), !isRot0Landscape);
inflateButtons(end, (ViewGroup) mRot0.findViewById(R.id.ends_group), isRot0Landscape);
inflateButtons(end, (ViewGroup) mRot90.findViewById(R.id.ends_group), !isRot0Landscape);
}
private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape) {
for (int i = 0; i < buttons.length; i++) {
inflateButton(buttons[i], parent, landscape);
}
}
protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape) {
LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
float size = extractSize(buttonSpec);
View v = createView(buttonSpec, parent, inflater, landscape); // 创建按键 View
if (v == null) return null;
if (size != 0) {
ViewGroup.LayoutParams params = v.getLayoutParams();
params.width = (int) (params.width * size);
}
parent.addView(v); // 添加按键 view
...
return v;
}
createView() 根据分割出的字符串(left,back;home;recent,right)创建对应的 View
private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater,
boolean landscape) {
View v = null;
String button = extractButton(buttonSpec);
...
if (HOME.equals(button)) {
v = inflater.inflate(R.layout.home, parent, false); // 主页键
} else if (BACK.equals(button)) {
v = inflater.inflate(R.layout.back, parent, false); // 返回键
} else if (RECENT.equals(button)) {
v = inflater.inflate(R.layout.recent_apps, parent, false); // 最近任务键
} else if (MENU_IME.equals(button)) {
...
}
return v;
}
2.3 导航栏按键监听
导航栏的返回、主页、最近任务键这三个按键分别对应 back.xml、home.xml 和 recent_apps.xml 这三个布局,它们使用的是自定义控件 KeyButtonView。
// frameworks\base\packages\SystemUI\res\layout\back.xml
<com.android.systemui.statusbar.policy.KeyButtonView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/back"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="4" // 映射KeyCode为4的KeyEvent
android:scaleType="center"
android:contentDescription="@string/accessibility_back"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"/>
// frameworks\base\packages\SystemUI\res\layout\home.xml
<com.android.systemui.statusbar.policy.KeyButtonView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/home"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="3" // 映射KeyCode为3的KeyEvent
android:scaleType="center"
android:contentDescription="@string/accessibility_home"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"/>
// frameworks\base\packages\SystemUI\res\layout\recent_apps.xml
<com.android.systemui.statusbar.policy.KeyButtonView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/recent_apps"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0" // recent_apps 没映射对应的 KeyEvent, 另外设置了监听器
android:scaleType="center"
android:contentDescription="@string/accessibility_recent"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"/>
以 Home 键为例,在 KeyButtonView 类的 onTouchEvent() 中捕获触摸事件。
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\policy\KeyButtonView.java
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
...
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownTime = SystemClock.uptimeMillis();
mLongClicked = false;
setPressed(true);
if (mCode != 0) { // 布局文件中的 systemui:keyCode
sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
} else {
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
...
break;
}
return true;
}
sendEvent() 负责调用 InputManager 模拟按键分发。
void sendEvent(int action, int flags, long when) {
final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
// public static final int KEYCODE_HOME = 3;
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);
// 模拟按键分发,如 KEYCODE_HOME,会在 PhoneWindowManager 中处理
InputManager.getInstance().injectInputEvent(ev,
InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
3 客制化
3.1 导航栏的创建/隐藏
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java
protected void makeStatusBarView() {
try {
// showNav 表示是否创建导航栏
boolean showNav = mWindowManagerService.hasNavigationBar();
if (showNav) {
createNavigationBar();
}
} catch (RemoteException ex) {
}
...
}
// frameworks\base\core\res\res\values\config.xml
public void setInitialDisplaySize(Display display, int width, int height, int density) {
// 修改这个配置文件中的config_showNavigationBar配置 frameworks\base\core\res\res\values\config.xml
mHasNavigationBar = res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
// 也可以在mk文件中添加新的属性来控制
String navBarOverride = SystemProperties.get("xxxx"); // qemu.hw.mainkeys
if ("1".equals(navBarOverride)) {
mHasNavigationBar = false;
} else if ("0".equals(navBarOverride)) {
mHasNavigationBar = true;
}
}
3.2 导航栏按键的增加与删除
以添加一个截图按键为例:
- NavigationBarView 类修改
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarView.java
public NavigationBarView(Context context, AttributeSet attrs) {
super(context, attrs);
updateIcons(context, Configuration.EMPTY, mConfiguration); // 截图图标也需要添加
// 添加 R.id.screenshot +
mButtonDispatchers.put(R.id.screenshot, new ButtonDispatcher(R.id.screenshot));
mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back));
mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home));
...
}
// +
public ButtonDispatcher getScreenshotButton() {
return mButtonDispatchers.get(R.id.screenshot);
}
private void prepareNavigationBarView() {
mNavigationBarView.reorient();
// 添加截图按键事件监听 +
ButtonDispatcher screenshotButton = mNavigationBarView.getScreenshotButton();
screenshotButton.setOnClickListener(this::onScreenshotClick);
screenshotButton.setLongClickable(true);
screenshotButton.setOnLongClickListener(this::onLongPressScreenshot);
...
}
private void onScreenshotClick(View v) { // 单击监听器
Log.e(TAG , "onScreenshotClick()");
}
private boolean onLongPressScreenshot(View v) { // 长按监听器
Log.e(TAG , "onLongPressScreenshot()");
return true;
}
private KeyButtonDrawable mScreenshotIcon; // 添加截图图标变量
private void updateIcons(Context ctx, Configuration oldConfig, Configuration newConfig) {
...
if (oldConfig.densityDpi != newConfig.densityDpi) {
// 添加图标 +
mScreenshotIcon = getDrawable(ctx,
R.drawable.ic_sysbar_screenshot,R.drawable.ic_sysbar_screenshot_dark);
mBackIcon = getDrawable(ctx, R.drawable.ic_sysbar_back, R.drawable.ic_sysbar_back_dark);
...
}
}
public void setNavigationIconHints(int hints, boolean force) {
// 设置截图按键图标
getScreenshotButton().setImageDrawable(mScreenshotIcon);
getBackButton().setImageDrawable(backIcon);
...
}
public void setDisabledFlags(int disabledFlags, boolean force) {
if (!force && mDisabledFlags == disabledFlags) return;
mDisabledFlags = disabledFlags;
final boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
boolean disableRecent = mUseCarModeUi
|| ((disabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0);
final boolean disableBack = ((disabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)
&& ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) == 0);
...
getBackButton().setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE);
getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE);
// 隐藏/显示
getScreenshotButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE);
...
}
- config.xml 文件修改
// frameworks\base\packages\SystemUI\res\values\config.xml
<!-- 增加 screenshot 字符串配置 -->
<string name="config_navBarLayout" translatable="false">left,back;home;recent,screenshot,right</string>
- NavigationBarInflaterView 类修改
// frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarInflaterView.java
public static final String SCREENSHOT = "screenshot";
private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater,
boolean landscape) {
if (HOME.equals(button)) {
v = inflater.inflate(R.layout.home, parent, false);
} else if (SCREENSHOT.equals(button)) { // 添加截图按键的判断 +
v = inflater.inflate(R.layout.screenshot, parent, false);
} else if (BACK.equals(button)) {
v = inflater.inflate(R.layout.back, parent, false);
} ...
}
- 添加 screenshot.xml 布局文件
// frameworks\base\packages\SystemUI\res\layout\screenshot.xml
<com.android.systemui.statusbar.policy.KeyButtonView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/screenshot"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
// 设置映射截图按键KeyCode ,这里和 PhoneWindowManager 保持一致,最终会模拟这个按键的分发
// 这里也可以设置为 0,如果要实现单击和长按的话
systemui:keyCode="128"
android:scaleType="center"
android:contentDescription="@string/accessibility_SCREENSHOT"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"
/>
更多推荐




所有评论(0)