需求
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.Factoryinterface 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@NavigationBarScopestatic NavigationBarFrame provideNavigationBarFrame(@DisplayId LayoutInflater layoutInflater) {return (NavigationBarFrame) layoutInflater.inflate(R.layout.navigation_bar_window, null);}@Provides@NavigationBarScopestatic 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.NavigationBarViewxmlns: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.NavigationBarInflaterViewandroid: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();// 获取返回按钮、主页、按钮图标drawableif (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中保存的viewinflateLayout(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); // 关键点:创建viewif (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.KeyButtonViewxmlns: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