欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-06-12 20:30:17
需求 基于mtk8163 8.1平台定制导航栏部分,在左边增加音量减,右边增加音量加 思路 需求开始做之前,一定要研读systemui navigation模块的代码流程!!!不...

需求

基于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 导航栏加载流程,希望对大家有所帮助