需求
基于 MTK8163 8.1 平台定制导航栏部分, 在左边增加音量减, 右边增加音量加, 需求定制步骤见下一文章.
思路
需求开始做之前, 一定要研读 SystemUI Navigation 模块的代码流程!!! 不要直接去网上 copy 别人改的需求代码, 盲改的话很容易出现问题, 然而无从解决. 网上有老平台 (8.0-) 的讲解 System UI 的导航栏模块的博客, 自行搜索. 8.0 对 System UI 还是做了不少细节上的改动, 代码改动体现上也比较多, 但是总体基本流程并没变.
源码阅读可以沿着一条线索去跟代码, 不要过分在乎代码细节! 例如我客制化这个需求, 可以跟着导航栏的返回 (back), 桌面(home), 最近任务(recent) 中的一个功能跟代码流程, 大体知道比如 recen 这个 view 是哪个方法调哪个方法最终加载出来, 加载的关键代码在哪, 点击事件怎么生成, 而不在意里面的具体逻辑判断等等.
代码流程
1.SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java;
从状态栏入口开始看.
- protected void makeStatusBarView() {
- final Context context = mContext;
- updateDisplaySize(); // populates mDisplayMetrics
- updateResources();
- updateTheme();
- ...
- ...
- try {
- boolean showNav = mWindowManagerService.hasNavigationBar();
- if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
- if (showNav) {
- createNavigationBar();// 创建导航栏
- }
- } catch (RemoteException ex) {
- }
- }
- 2.SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java;
进入 createNavigationBar 方法, 发现主要是用 NavigationBarFragment 来管理.
- protected void createNavigationBar() {
- mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {
- mNavigationBar = (NavigationBarFragment) fragment;
- if (mLightBarController != null) {
- mNavigationBar.setLightBarController(mLightBarController);
- }
- mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
- });
- }
- 3.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarFragment.java;
看 NavigationBarFragment 的 create 方法, 终于知道, 是 WindowManager 去 addView 了导航栏的布局, 最终 add 了 fragment 的 onCreateView 加载的布局.(其实 SystemUI 所有的模块都是 WindowManager 来加载 View.)
- 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,
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.token = new Binder();
- lp.setTitle("NavigationBar");
- lp.windowAnimations = 0;
- View navigationBarView = LayoutInflater.from(context).inflate(
- R.layout.navigation_bar_window, null);
- if (DEBUG) Log.v(TAG, "addNavigationBar: about to add" + navigationBarView);
- 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) // 注意! fragment 里 onCreateView 加载的布局是 add 到这个 Window 属性的 view 里的.
- .commit();
- fragmentHost.addTagListener(TAG, listener);
- return navigationBarView;
- }
- }
- 4.SystemUI\res\layout\navigation_bar_window.xml;
来看 WindowManager 加载的这个 view 的布局: navigation_bar_window.xml, 发现根布局是自定义的 view 类 NavigationBarFrame.(其实 SystemUI 以及其他系统应用如 Launcher, 都是这种自定义 view 的方式, 好多逻辑处理也都是在自定义 view 里, 不能忽略)
<com.android.systemui.statusbar.phone.NavigationBarFrame
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:systemui="http://schemas.android.com/apk/res-auto"
- android:id="@+id/navigation_bar_frame"
- android:layout_height="match_parent"
- android:layout_width="match_parent">
- </com.android.systemui.statusbar.phone.NavigationBarFrame>
- 5.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarFrame.java;
我们进入 NavigationBarFrame 类. 发现类里并不是我们的预期, 就是一个 FrameLayout, 对 DeadZone 功能下的 touch 事件做了手脚, 不管了.
6. 再回来看看 NavigationBarFragment 的生命周期呢. onCreateView()里, 导航栏的真正的 rootView.
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(R.layout.navigation_bar, container, false);
- }
- 7.SystemUI\res\layout\navigation_bar.xml;
进入导航栏的真正根布局: navigation_bar.xml, 好吧又是自定义 view,NavigationBarView 和 NavigationBarInflaterView 都要仔细研读.
<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>
8.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarInflaterView.java; 继承自 FrameLayout
先看构造方法, 因为加载 xml 布局首先走的是初始化
- public NavigationBarInflaterView(Context context, AttributeSet attrs) {
- super(context, attrs);
- createInflaters();// 根据屏幕旋转角度创建子 view(单个 back home or recent)的父布局
- Display display = ((WindowManager)
- context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
- Mode displayMode = display.getMode();
- isRot0Landscape = displayMode.getPhysicalWidth()> displayMode.getPhysicalHeight();
- }
- 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);
- updateAlternativeOrder();
- }
再看 onFinishInflate()方法, 这是 view 的生命周期, 每个 view 被 inflate 之后都会回调.
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- inflateChildren();// 进去看无关紧要 忽略
- clearViews();// 进去看无关紧要 忽略
- inflateLayout(getDefaultLayout());// 关键方法: 加载了 back.home.recent 三个按钮的 layout
- }
看 inflateLayout(): 里面的 newLayout 参数很重要!!! 根据上一个方法看到 getDefaultLayout(), 他 return 了一个在 xml 写死的字符串. 再看 inflateLayout 方法, 他解析分割了 xml 里配置的字符串, 并传给了 inflateButtons 方法
- protected void inflateLayout(String newLayout) {
- mCurrentLayout = newLayout;
- if (newLayout == null) {
- newLayout = getDefaultLayout();
- }
- String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);// 根据 ";" 号分割成长度为 3 的数组
- String[] start = sets[0].split(BUTTON_SEPARATOR);// 根据 "," 号分割, 包含 left[.5W]和 back[1WC]
- String[] center = sets[1].split(BUTTON_SEPARATOR);// 包含 home
- String[] end = sets[2].split(BUTTON_SEPARATOR);// 包含 recent[1WC]和 right[.5W]
- // Inflate these in start to end order or accessibility traversal will be messed up.
inflateButtons(start, mRot0.findViewById(R.id.ends_group), isRot0Landscape, true);
inflateButtons(start, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, true);
inflateButtons(center, mRot0.findViewById(R.id.center_group), isRot0Landscape, false);
inflateButtons(center, mRot90.findViewById(R.id.center_group), !isRot0Landscape, false);
- addGravitySpacer(mRot0.findViewById(R.id.ends_group));
- addGravitySpacer(mRot90.findViewById(R.id.ends_group));
inflateButtons(end, mRot0.findViewById(R.id.ends_group), isRot0Landscape, false);
inflateButtons(end, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, false);
- }
- protected String getDefaultLayout() {
- return mContext.getString(R.string.config_navBarLayout);
- }
- //SystemUI\res\values\config.xml
- <!-- Nav bar button default ordering/layout -->
- <string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>
再看 inflateButtons()方法, 遍历加载 inflateButton:
- private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape,
- boolean start) {
- for (int i = 0; i <buttons.length; i++) {
- inflateButton(buttons[i], parent, landscape, start);
- }
- }
- @Nullable
- 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);//addView 到父布局
- addToDispatchers(v);
- View lastView = landscape ? mLastLandscape : mLastPortrait;
- View accessibilityView = v;
- if (v instanceof ReverseFrameLayout) {
- accessibilityView = ((ReverseFrameLayout) v).getChildAt(0);
- }
- if (lastView != null) {
- accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
- }
- if (landscape) {
- mLastLandscape = accessibilityView;
- } else {
- mLastPortrait = accessibilityView;
- }
- return v;
- }
我们来看 createView()方法: 以 home 按键为例, 加载了 home 的 button, 其实是加载了 R.layout.home 的 layout 布局
- private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
- View v = null;
- ...
- ...
- 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)) {
- v = inflater.inflate(R.layout.menu_ime, parent, false);
- } else if (NAVSPACE.equals(button)) {
- v = inflater.inflate(R.layout.nav_key_space, parent, false);
- } else if (CLIPBOARD.equals(button)) {
- v = inflater.inflate(R.layout.clipboard, parent, false);
- }
- ...
- ...
- return v;
- }
- //SystemUI\res\layout\home.xml
- // 这里布局里没有 src 显示 home 的 icon, 肯定是在代码里设置了
- // 这里也是自定义 view:KeyButtonView
- <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"// 引用了 dimens.xml 里的 navigation_key_width
- android:layout_height="match_parent"
- android:layout_weight="0"
- systemui:keyCode="3"//systemui 自定义的属性
- android:scaleType="fitCenter"
- android:contentDescription="@string/accessibility_home"
- android:paddingTop="@dimen/home_padding"
- android:paddingBottom="@dimen/home_padding"
- android:paddingStart="@dimen/navigation_key_padding"
- android:paddingEnd="@dimen/navigation_key_padding"
- />
9.SystemUI\src\com\android\systemui\statusbar\policy\KeyButtonView.java 先来看 KeyButtonView 的构造方法: 我们之前 xml 的 systemui:keyCode="3" 方法在这里获取. 再来看 Touch 事件, 通过 sendEvent()方法可以看出, back 等 view 的点击 touch 事件不是自己处理的, 而是交由系统以实体按键 (keycode) 的形式处理的.
当然 KeyButtonView 类还处理了支持长按的 button, 按键的响声等, 这里忽略.
至此, 导航栏按键事件我们梳理完毕.
- 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);
- mPlaySounds = a.getBoolean(R.styleable.KeyButtonView_playSound, true);
- TypedValue value = new TypedValue();
- if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {
- mContentDescriptionRes = value.resourceId;
- }
- a.recycle();
- setClickable(true);
- mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
- mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- mRipple = new KeyButtonRipple(context, this);
- setBackground(mRipple);
- }
- ...
- ...
- public boolean onTouchEvent(MotionEvent ev) {
- ...
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- mDownTime = SystemClock.uptimeMillis();
- mLongClicked = false;
- setPressed(true);
- if (mCode != 0) {
- sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);// 关键方法
- } else {
- // Provide the same haptic feedback that the system offers for virtual keys.
- performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
- }
- playSoundEffect(SoundEffectConstants.CLICK);
- removeCallbacks(mCheckLongPress);
- postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
- break;
- ...
- ...
- }
- return true;
- }
- void sendEvent(int action, int flags, long when) {
- mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT)
- .setType(MetricsEvent.TYPE_ACTION)
- .setSubtype(mCode)
- .addTaggedData(MetricsEvent.FIELD_NAV_ACTION, action)
- .addTaggedData(MetricsEvent.FIELD_FLAGS, flags));
- final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
- // 这里根据 mCode new 了一个 KeyEvent 事件, 通过 injectInputEvent 使事件生效.
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);
- }
10. 还遗留一个问题: 设置图片的 icon 到底在哪? 我们之前一直阅读的是 NavigationBarInflaterView, 根据布局我们还有一个类没有看, NavigationBarView.java
SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarView.java;
进入 NavigationBarView 类里, 找到构造方法.
- public NavigationBarView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mDisplay = ((WindowManager) context.getSystemService(
- Context.WINDOW_SERVICE)).getDefaultDisplay();
- ...
- ...
- updateIcons(context, Configuration.EMPTY, mConfiguration);// 关键方法
- mBarTransitions = new NavigationBarTransitions(this);
- //mButtonDispatchers 是维护这些 home back recent 图标 view 的管理类, 会传递到他的 child,NavigationBarInflaterView 类中
- 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));
- }
- private void updateIcons(Context ctx, Configuration oldConfig, Configuration newConfig) {
- ...
- iconLight = mNavBarPlugin.getHomeImage(
- ctx.getDrawable(R.drawable.ic_sysbar_home));
- iconDark = mNavBarPlugin.getHomeImage(
- ctx.getDrawable(R.drawable.ic_sysbar_home_dark));
- //mHomeDefaultIcon = getDrawable(ctx,
- // R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home_dark);
- mHomeDefaultIcon = getDrawable(iconLight,iconDark);
- // 亮色的 icon 资源
- iconLight = mNavBarPlugin.getRecentImage(
- ctx.getDrawable(R.drawable.ic_sysbar_recent));
- // 暗色的 icon 资源
- iconDark = mNavBarPlugin.getRecentImage(
- ctx.getDrawable(R.drawable.ic_sysbar_recent_dark));
- //mRecentIcon = getDrawable(ctx,
- // R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark);
- mRecentIcon = getDrawable(iconLight,iconDark);
- mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu,
- R.drawable.ic_sysbar_menu_dark);
- ...
- ...
- }
11. 从第 10 可以看到, 以 recent 为例, 在初始化时得到了 mRecentIcon 的资源, 再看谁调用了了 mRecentIcon 就可知道, 即反推看调用流程.
- private void updateRecentsIcon() {
- getRecentsButton().setImageDrawable(mDockedStackExists ? mDockedIcon : mRecentIcon);
- mBarTransitions.reapplyDarkIntensity();
- }
updateRecentsIcon 这个方法设置了 recent 图片的资源, 再看谁调用了 updateRecentsIcon 方法: onConfigurationChanged 屏幕旋转会重新设置资源图片
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- boolean uiCarModeChanged = updateCarMode(newConfig);
- updateTaskSwitchHelper();
- updateIcons(getContext(), mConfiguration, newConfig);
- updateRecentsIcon();
- if (uiCarModeChanged || mConfiguration.densityDpi != newConfig.densityDpi
- || mConfiguration.getLayoutDirection() != newConfig.getLayoutDirection()) {
- // If car mode or density changes, we need to reset the icons.
- setNavigationIconHints(mNavigationIconHints, true);
- }
- mConfiguration.updateFrom(newConfig);
- }
- public void setNavigationIconHints(int hints, boolean force) {
- ...
- ...
- mNavigationIconHints = hints;
- // We have to replace or restore the back and home button icons when exiting or entering
- // carmode, respectively. Recents are not available in CarMode in nav bar so change
- // to recent icon is not required.
- KeyButtonDrawable backIcon = (backAlt)
- ? getBackIconWithAlt(mUseCarModeUi, mVertical)
- : getBackIcon(mUseCarModeUi, mVertical);
- getBackButton().setImageDrawable(backIcon);
- updateRecentsIcon();
- ...
- ...
- }
reorient()也调用了 setNavigationIconHints()方法:
- public void reorient() {
- updateCurrentView();
- ...
- setNavigationIconHints(mNavigationIconHints, true);
- getHomeButton().setVertical(mVertical);
- }
再朝上推, 最终追溯到 NavigationBarFragment 的 onConfigurationChanged()方法 和 NavigationBarView 的 onAttachedToWindow()和 onSizeChanged()方法. 也就是说, 在 NavigationBarView 导航栏这个布局加载的时候就会设置图片资源, 和长度改变, 屏幕旋转都有可能引起重新设置
至此, SystemUI 的虚拟导航栏模块代码流程结束.
总结
创建一个 window 属性的父 view
通过读取解析 xml 里 config 的配置, addView 需要的 icon, 或者调换顺序
src 图片资源通过代码设置亮色和暗色
touch 事件以 keycode 方式交由系统处理
下面一节介绍音量加减的定制步骤.
来源: https://juejin.im/post/5ad9a077f265da0b767d0669