解析Android 8.1平台SystemUI 导航栏加载流程

作者:cczhengv 时间:2023-06-23 15:21:21 

需求

基于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.进入 createNavigationBar 方法,发现主要是用 NavigationBarFragment 来管理.

protected void createNavigationBar() {
 mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {
   mNavigationBar = (NavigationBarFragment) fragment;
   if (mLightBarController != null) {
     mNavigationBar.setLightBarController(mLightBarController);
   }
   mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
 });
}


3.看 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);
}


进入导航栏的真正根布局: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>

7.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"/>

8.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);
}

9.还遗留一个问题:设置图片的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);
   ...
   ...
}

10.从第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的虚拟导航栏模块代码流程结束。

总结

  1. 创建一个window属性的父view

  2. 通过读取解析xml里config的配置,addView需要的icon,或者调换顺序

  3. src图片资源通过代码设置亮色和暗色

  4. touch事件以keycode方式交由系统处理

以上所述是小编给大家介绍的Android 8.1平台SystemUI 导航栏加载流程,希望对大家有所帮助

来源:https://blog.csdn.net/u012932409/article/details/89498082

标签:android,导航栏,加载,systemui
0
投稿

猜你喜欢

  • SpringBoot整合ES解析搜索返回字段问题

    2023-01-26 21:39:53
  • Jenkins使用Gradle编译Android项目详解

    2021-12-30 22:26:30
  • C#基于socket模拟http请求的方法

    2022-09-12 09:55:11
  • android中UI主线程与子线程深入分析

    2022-02-06 15:19:25
  • 详解Android通知栏沉浸式/透明化完整解决方案

    2023-09-06 03:59:11
  • SpringBoot整合OpenApi的实践

    2023-08-03 11:59:55
  • C#使用Task实现异步方法

    2022-09-02 20:26:53
  • spring mvc高级技术实例详解

    2022-10-11 12:50:15
  • Android中SurfaceView用法简单实例

    2022-01-05 20:10:11
  • Java数组动态增加容量过程解析

    2023-06-07 07:35:24
  • Spring Boot修改启动端口的方法

    2022-02-10 05:49:55
  • Mybatis如何通过接口实现sql执行原理解析

    2022-11-30 11:31:26
  • java如何反编译jar包并修改class文件重新打包

    2021-09-20 05:24:26
  • Winform 控件优化LayeredWindow无锯齿圆角窗体

    2021-12-07 22:54:17
  • Hibernate hql查询代码实例

    2021-07-24 18:03:33
  • WPF中使用WebView2控件的方法及常见问题

    2023-10-02 18:23:28
  • Android文本输入框(EditText)输入密码时显示与隐藏

    2022-04-24 06:23:47
  • Java泛型类与泛型方法的定义详解

    2023-11-25 01:29:22
  • Android activity堆栈及管理实例详解

    2022-07-28 01:17:14
  • Android实战打飞机游戏之无限循环的背景图(2)

    2023-11-11 23:33:44
  • asp之家 软件编程 m.aspxhome.com