千家信息网

Android怎么实现SystemUI导航栏加载

发表于:2025-01-20 作者:千家信息网编辑
千家信息网最后更新 2025年01月20日,今天小编给大家分享一下Android怎么实现SystemUI导航栏加载的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所
千家信息网最后更新 2025年01月20日Android怎么实现SystemUI导航栏加载

今天小编给大家分享一下Android怎么实现SystemUI导航栏加载的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

需求

基于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里,不能忽略)

 

5.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarFrame.java;

我们进入NavigationBarFrame类。发现类里并不是我们的预期,就是一个FrameLayout,对DeadZone功能下的touch事件做了手脚,不管了。

6.再回来看看NavigationBarFragment的生命周期呢。onCreateView()里,导航栏的真正的rootView。

@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,    Bundle savedInstanceState) {  return inflater.inflate(R.layout.navigation_bar, container, false);}

进入导航栏的真正根布局:navigation_bar.xml,好吧又是自定义view,NavigationBarView 和 NavigationBarInflaterView 都要仔细研读。

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之后都会回调。

@Overrideprotected 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

 left[.5W],back[1WC];home;recent[1WC],right[.5W]

再看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);  }}@Nullableprotected 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

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屏幕旋转会重新设置资源图片

@Overrideprotected 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导航栏这个布局加载的时候就会设置图片资源,和长度改变,屏幕旋转都有可能引起重新设置

以上就是"Android怎么实现SystemUI导航栏加载"这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注行业资讯频道。

0