Android 8.1 SystemUI虚拟导航键加载流程解析
需求
基于mtk 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的虚拟导航栏模块代码流程结束。
总结
- 创建一个window属性的父view
- 通过读取解析xml里config的配置,addview需要的icon,或者调换顺序
- src图片资源通过代码设置亮色和暗色
- touch事件以keycode方式交由系统处理
上一篇: 盘点2015年手机界的六大黑科技
下一篇: Yii2 框架跑脚本时内存泄漏问题分析