SystemUI中NavigationBar分析
需求
SystemUI是一个与系统组件显示紧密相关的应用,包含快捷中心、消息通知、状态栏、导航栏、任务中心等诸多模块,本文介绍NavigationBar模块。SystemUI源码位于/frameworks/base/packages/SystemUI,Android13平台。NavigationBar显示如下:
关键类
- NavigationBarComponent.java:NavigationBar组件类,采用Dagger进行依赖注入
- NavigationBar.java:将导航栏view添加到window
- navigation_bar.xml:NavigationBar布局文件
- NavigationBarView.java:设置导航栏图标
- NavigationBarInflaterView:解析config中导航栏排布信息,创建对应的view
- home.xml/back.xml:导航栏按钮对应的布局
- KeyButtonView.java:导航栏图标的View,如果设置了keycode,则将点击事件touch以keycode方式交由系统处理
代码流程
1. NavigationBar模块启动
Android13平台的SystemUI代码较旧平台变化比较大,各个组件采用了Dagger进行依赖注入(DI)。在SystemUIApplication启动的时候进行了组件的初始化,NavigationBar组件如下:
// SystemUI\src\com\android\systemui\navigationbar\NavigationBarComponent.java
@Subcomponent(modules = { NavigationBarModule.class })
@NavigationBarComponent.NavigationBarScope
public interface NavigationBarComponent {
@Subcomponent.Factory
interface Factory {
NavigationBarComponent create(
@BindsInstance @DisplayId Context context,
@BindsInstance @Nullable Bundle savedState);
}
NavigationBar getNavigationBar();
}
// SystemUI\src\com\android\systemui\navigationbar\NavigationBarModule.java
@Module
public interface NavigationBarModule {
@Provides
@NavigationBarScope
static NavigationBarFrame provideNavigationBarFrame(@DisplayId LayoutInflater layoutInflater) {
return (NavigationBarFrame) layoutInflater.inflate(R.layout.navigation_bar_window, null);
}
@Provides
@NavigationBarScope
static NavigationBarView provideNavigationBarview(
@DisplayId LayoutInflater layoutInflater, NavigationBarFrame frame) {
View barView = layoutInflater.inflate(R.layout.navigation_bar, frame);
return barView.findViewById(R.id.navigation_bar_view);
}
}
从上面可以看到navigation_bar是布局文件,NavigationBarView是具体的view,NavigationBar中实现导航栏view添加到window。
2.布局文件navigation_bar.xml
NavigationBarView和NavigationBarInflaterView实际上都是Framelayout
// SystemUI\res\layout\navigation_bar.xml
<com.android.systemui.navigationbar.NavigationBarView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/navigation_bar_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:background="@drawable/system_bar_background">
<com.android.systemui.navigationbar.NavigationBarInflaterView
android:id="@+id/navigation_inflater"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" />
</com.android.systemui.navigationbar.NavigationBarView>
3.NavigationBarView
我们接着看NavigationBarView,主要做了下面几件事情:
- 在构造方法中创建了返回、主页等ButtonDispatcher。
- 布局加载完成时,找到了子view(NavigationInflaterView),并将ButtonDispatcher设置给了NavigationInflaterView
- onAttachedToWindow()时,将对应的图标设置给返回、主页等view
我们发现NavigationBarView中并没有创建返回、主页等对应的view,将返回、主页等对应的view添加到ViewGroup的操作在NavigationInflaterView中
// SystemUI\src\com\android\systemui\navigationbar\NavigationBarView.java
// 创建ButtonDispatcher
public NavigationBarView(Context context, AttributeSet attrs) {
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));
}
// 布局加载完成
public void onFinishInflate() {
super.onFinishInflate();
mNavigationInflaterView = findViewById(R.id.navigation_inflater);
mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
reloadNavIcons();// reloadNavIcons()中调用了updateIcons()
}
// 获取图标
private void updateIcons(Configuration oldConfig) {
final boolean orientationChange = oldConfig.orientation != mConfiguration.orientation;
final boolean densityChange = oldConfig.densityDpi != mConfiguration.densityDpi;
final boolean dirChange = oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirectin();
// 获取返回按钮、主页、按钮图标drawable
if (orientationChange || densityChange) {
mDockedIcon = getDrawable(R.drawable.ic_sysbar_docked);
mHomeDefaultIcon = getHomeDrawable();
}
if (densityChange || dirChange) {
mRecentIcon = getDrawable(R.drawable.ic_sysbar_recent);
mContextualButtonGroup.updateIcons(mLightIconColor, mDarkIconColor);
}
if (orientationChange || densityChange || dirChange) {
mBackIcon = getBackDrawable();
}
}
// 返回按钮图标,KeyButtonDrawable实际上是一个Drawable
public KeyButtonDrawable getBackDrawable() {
KeyButtonDrawable drawable = getDrawable(getBackDrawableRes());
orientBackButton(drawable);
return drawable;
}
// 设置图标
protected void onAttachedToWindow() {
super.onAttachedToWindow();
requestApplyInsets();
reorient();
updateNavButtonIcons();
}
4.NavigationBarInflaterView
NavigationBarInflaterView是真正创建返回、主页按钮view的地方,先解析config中设置config_navBarLayout排列信息,然后通过对应layout创建KeyButtonView。部分代码如下:
// SystemUI\src\com\android\systemui\navigationbar\NavigationBarInflaterView.java
// 布局加载完成
protected void onFinishInflate() {
super.onFinishInflate();
inflateChildren(); // 加载布局
clearViews();// 清空传递过来的ButtonDispatcher中保存的view
inflateLayout(getDefaultLayout()); // 关键点:加载布局,创建view
}
// getDefaultLayout()是获取按钮排布信息,从config.xml中获取,如:<string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>
// 解析newLayout创建view
protected void inflateLayout(String newLayout) {
if (newLayout == null) {
newLayout = getDefaultLayout();
}
String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
if (sets.length != 3) {
Log.d(TAG, "Invalid layout.");
newLayout = getDefaultLayout();
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);
// Inflate these in start to end order or accessibility traversal will be messed up.
inflateButtons(start, mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_ends_group),
false /* landscape */, true /* start */);
inflateButtons(center, mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_center_group),
false /* landscape */, false /* start */);
addGravitySpacer(mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_ends_group));
inflateButtons(end, mHorizontal.findViewById(
com.android.internal.R.id.input_method_nav_ends_group),
false /* landscape */, false /* start */);
updateButtonDispatchersCurrentView();
}
// 创建view并添加到viewgroup
protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
boolean start) {
LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
View v = createView(buttonSpec, parent, inflater); // 关键点:创建view
if (v == null) return null;
v = applySize(v, buttonSpec, landscape, start);
parent.addView(v);
addToDispatchers(v);
View lastView = landscape ? mLastLandscape : mLastPortrait;
View accessibilityView = v;
if (v instanceof ReverseRelativeLayout) {
accessibilityView = ((ReverseRelativeLayout) v).getChildAt(0);
}
if (lastView != null) {
accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
}
if (landscape) {
mLastLandscape = accessibilityView;
} else {
mLastPortrait = accessibilityView;
}
return v;
}
// 通过对应的布局创建view,实际上创建的是KeyButtonView
View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
View v = null;
String button = extractButton(buttonSpec);
if (LEFT.equals(button)) {
button = extractButton(NAVSPACE);
} else if (RIGHT.equals(button)) {
button = extractButton(MENU_IME_ROTATE);
}
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);
}
return v;
}
5.KeyButtonView
如上一步back按钮的布局文件如下。
<com.android.systemui.navigationbar.buttons.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"
android:scaleType="center"
android:contentDescription="@string/accessibility_back"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"
/>
KeyButtonView是一个ImageView,重写了onTouchEvent,设置了keyCode,则点击后给系统发送对应的keyevent
// SystemUI\src\com\android\systemui\navigationbar\buttons\KeyButtonView.java
public boolean onTouchEvent(MotionEvent ev) {
...
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mCode != KEYCODE_UNKNOWN) {
sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
} else {
// Provide the same haptic feedback that the system offers for virtual keys.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
}
}
private 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);
int displayId = INVALID_DISPLAY;
if (getDisplay() != null) {
displayId = getDisplay().getDisplayId();
}
if (displayId != INVALID_DISPLAY) {
ev.setDisplayId(displayId);
}
mInputManager.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
总结
- 将导航栏View添加到Window进行显示
- 通过读取解析xml里config的图标排布信息,来创建对应的view
- 如果设置了keycode,则将点击事件touch以keycode方式交由系统处理
参考
- Dagger/Hilt依赖注入使用:https://developer.android.com/training/dependency-injection?hl=zh-cn
- 解析Android 8.1平台SystemUI 导航栏加载流程:https://www.jb51.net/article/174313.htm