我们使用的大多数android 手机上的Home 键,返回键以及menu 键都是实体触摸感应按键。如果你用Google 的Nexus4 或Nexus5 话,你会发现它们并没有实体按键或触摸感应按键,取而代之的是在屏幕的下方加了一个小黑条,在这个黑条上有3 个按钮控件,这种设置无疑使得手机的外观的设计更加简约。但我遇到身边用Nexus 4 手机的人都吐槽这种设计,原因很简单:好端端的屏幕,被划出一块区域用来显示3 个按钮:Back, Home, Recent 。并且它一直用在那里占用着。
在android 源码中,那一块区域被叫做NavigationBar 。同时,google 在代码中也预留了标志,用来控制它的显示与隐藏。NavigationBar 的显示与隐藏的控制是放在SystemUI 中的,具体的路径是:\frameworks\base\packages\SystemUI 。对android4.0 以上的手机而言, SystemUi 包含两部分:StatusBar 和NavigationBar 。在SystemUI 的工程下有一个类PhoneStatusBar.java ,在该类中可以发现关于控制NavigationBar 的相关代码:
在start() 方法里可以看到NavigationBar 是在那时候被添加进来,但只是添加,决定它显示还是隐藏是在后面控制的。@Override public void start() { mDisplay = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); updateDisplaySize(); /// M: Support Smartbook Feature. if (SIMHelper.isMediatekSmartBookSupport()) { /// M: [ALPS01097705] Query the plug-in state as soon as possible. mIsDisplayDevice = SIMHelper.isSmartBookPluggedIn(mContext); Log.v(TAG, "start, mIsDisplayDevice=" + mIsDisplayDevice); } super.start(); // calls createAndAdd Windows() function。 addNavigationBar(); // Lastly, call to the icon policy to install/update all the icons. mIconPolicy = new PhoneStatusBarPolicy(mContext); mHeadsUpObserver.onChange(true); // set up if (ENABLE_HEADS_UP) { mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(SETTING_HEADS_UP), true, mHeadsUpObserver); } }
// For small-screen devices (read: phones) that lack hardware navigation buttons private void addNavigationBar() { if (DEBUG) Slog.v(TAG, "addNavigationBar: about to add " + mNavigationBarView); if (mNavigationBarView == null) return; //表示在Frame没有时不进行下一步操作。 prepareNavigationBarView(); mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams()); }
try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Slog.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { mNavigationBarView = (NavigationBarView) View.inflate(context, R.layout.navigation_bar, null); mNavigationBarView.setDisabledFlags(mDisabled); mNavigationBarView.setBar(this); } } catch (RemoteException ex) { // no window manager? good luck with that }
@Override public boolean hasNavigationBar() { return mPolicy.hasNavigationBar(); }Policy 向下调用实际上调用的是PhoneWindowManager 实现的hasNavigationBar 方法,下面代码是PhoneWindowManager 中的 hasNavigationBar() 方法。
// Use this instead of checking config_showNavigationBar so that it can be consistently // overridden by qemu.hw.mainkeys in the emulator. public boolean hasNavigationBar() { return mHasNavigationBar; }
if (!mHasSystemNavBar) { mHasNavigationBar = mContext.getResources().getBoolean( com.android.internal.R.bool.config_showNavigationBar); // Allow a system property to override this. Used by the emulator. // See also hasNavigationBar(). String navBarOverride = SystemProperties.get("qemu.hw.mainkeys"); if (! "".equals(navBarOverride)) { if (navBarOverride.equals("1")) mHasNavigationBar = false; else if (navBarOverride.equals("0")) mHasNavigationBar = true; } } else { mHasNavigationBar = false; }
PhoneWindowManager.java中:
String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
该配置项所在目录一般在:/system/ build.prop中。
所以上面的两处设定共同决定了NavigationBar的显示与隐藏。
NavigationBar可以在开机后根据需要显示或隐藏,比如在打开某个应用隐藏,打开另一应用显示。
修改步骤:
1) ActivityStack.java中的 resumeTopActivityLocked 是所有启动应用的启动的入口,所以在这里添加进入的入口。
2) 在PhoneWindowManager.java中的 mHasNavigationBar 是显示与否的标志,肯定要修改,而这里的修改应该在WindowManagerService.java中进行,因为,WindowManagerService中的mPolicy是操作PhoneWindowManager的接口,这样不会破坏封装,所以1)中要添加调入到WMS中的接口,WMS然后再调入PWM。
3) PWM(PhoneWindowManager)中有mStatusBarService,之所以用这个服务,是因为不破坏封装和同步。
4) StatusBarManagerService 中添加显示消失的接口,同理在Client端也要添加相应的显示和消失接口,具体在 CommandQueue和PhoneStatusBar中。
5) PhoneStatusBar中添加显示和消失的逻辑。
public void showNavigationBar() { Xlog.d(TAG, " showNavigationBar "); if (mNavigationBarView == null) { try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Slog.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { mNavigationBarView = (NavigationBarView) View.inflate(mContext, R.layout.navigation_bar, null); mNavigationBarView.setDisabledFlags(mDisabled); mNavigationBarView.setBar(this); } mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams()); } catch (RemoteException ex) { // no window manager? good luck with that } } } public void hideNavigationBar() { Xlog.d(TAG, " hideNavigationBar "); if ( mNavigationBarView != null) { mWindowManager.removeView(mNavigationBarView); } mNavigationBarView = null; }
从AMS--> WMS-->PMS --> StatusbarManagerService--> CommandQueue(callback) -->
PhoneStatusBar
也许有人会说,这样的调用很繁琐,为啥不用广播呢?
原因很直接:广播显然是在不同的线程里面,这样做不能保证窗口同步刷新,layout以后的后果未知。
2) 修改当然也会有问题,问题的发生在Launcher和应用切换间。Navigationbar的添加与否会影响Configuration的变化,这里的Configuration不单包括oritation的变化(其实这里没有oritation的变化),在实际中自写Launcher没有NV bar,android的原生应用有NV bar,发现一个问题,在从MMs返回自写Launcher时,会重新调用Launcher的onCreate函数,导致原本进入了GridView的,结果停留在Home的壁纸界面;而其它的应用如计算器就会直接进如gridview页面。问题的原因就是由于应用的fullscreen和Launcher 的Configuration变化引起的。
a. 解释下MMs和计算器的区别,实际上就是fullscreen的区别,通常来说和透明度相关
详细参见ActivityRecord()@ActivityRecord.java
MMs: fullscreen == false
Caculator: fullscreen == true
b. 解释下上面问题的原因,想象一下这样的情景,从GridView进入MMS中,
1)Launcher(fullscreen) Paused
2)launch MMs, Resumed
3)由于MMs不是fullscreen,所以在ensureActivitiesVisibleLocked中会去检查当前的top应用是否全屏,如果不是全屏,则会把下边的Acitivity show出来,此刻,下面的Activity为Launcher,而且Launcher一直是启动的,所以这里调用relaunchActivityLocked,这会导致重新的onCreate:
java.lang.Exceptioncom.android.server.am.ActivityStack.relaunchActivityLocked(ActivityStack.java:5374) :at com.android.server.am.ActivityStack.ensureActivityConfigurationLocked(ActivityStack.java:5339) :at com.android.server.am.ActivityStack.ensureActivitiesVisibleLocked(ActivityStack.java:1607) :at com.android.server.am.ActivityStack.ensureActivitiesVisibleLocked(ActivityStack.java:1727) :at com.android.server.am.ActivityStack.completeResumeLocked(ActivityStack.java:1538) :at com.android.server.am.ActivityStack.realStartActivityLocked(ActivityStack.java:865) :at com.android.server.am.ActivityManagerService.attachApplicationLocked(ActivityManagerService.java:4958) :at com.android.server.am.ActivityManagerService.attachApplication(ActivityManagerService.java:5027) :at android.app.ActivityManagerNative.onTransact(ActivityManagerNative.java:387) :at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:1908) :at android.os.Binder.execTransact(Binder.java:351) :at dalvik.system.NativeStart.run(Native Method)
4) 正常情况下,就算对Launcher调用了ensureActivityConfigurationLocked
也不会刷新屏幕,从而进入launcher的onCeate流程,原因在下面就返回了:
if (r.configuration == newConfig && !r.forceNewConfig) { if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG, "Configuration unchanged in " + r); return true; }
但是这里Navigation bar的从有到无必然会导致前面所说的Configuration的变化,Configuration的变化如下:
[email protected] newConfig = {1.0 ?mcc?mnc en_USldltr sw360dp w360dp h567dp 320dpi nrml port finger -keyb/v/h -nav/hskin=/system/framework/framework-res.apk s.9}, r.configuration = {1.0?mcc?mnc en_US ldltr sw360dp w360dp h615dp 320dpi nrml long portfinger -keyb/v/h -nav/h skin=/system/framework/framework-res.apks.8}, r.forceNewConfig = false
细心的读者可能发现屏幕的高度h567dp 和 h615dp 发生了变化,差别就是NavigationBar的高度,由于这样原因,这下真的就要调用relaunchActivityLocked来刷新后面的Activity了(Launcher)
问题的原因已经分析出来,修改也就简单了
final boolean ensureActivityConfigurationLocked(ActivityRecord r, int globalChanges) { if ((changes&(~r.info.getRealConfigChanged())) != 0 || r.forceNewConfig) { if ((r.packageName.contains("com.your.packagename") && (changes == 0x500 || changes == 0x580))) { // cancel relauncher ifyour package r.configChangeFlags = 0; //r.stopFreezingScreenLocked(false); return true; } .... } }
在实际的应用中,发现没有Navigation Bar的横屏Launcher在重新启动后的第一次显示,会把NavigationBar显示出来,这里解释一下为什么会出现此现象,问题发生的原因其实和PhoneWindowManager(PWM)中的mStatusBarService的值有关,在某些时候,mStatusBarService 可能为null,StatusBarManagerService 依赖WMS(参见SystemServer.java), WMS中才能调用PWM,而hideNavigation Bar又是从PWM中调入,必然在中间PWM中mStatusBarSerivice有时刻为空,导致会先显示navigation bar然后再消失。
解决方案:
1) 取消默认的navigation bar显示 【在config.xml 中取消】
2) PWM中判断mStatusBarService是否为空的逻辑
public void showNavigationBar() { Slog.d(TAG, " PWM showNavigationBar xxxx hasNavigationBar = " + hasNavigationBar()); if (!hasNavigationBar()) { //TODO: need open it if (mStatusBarService != null) { mHasNavigationBar = true; // wait for mStatusBarService prepared try { Slog.d(TAG, " PWM showNavigationBar mStatusBarService xxxx"); mStatusBarService.showNavigationBar(); } catch (RemoteException e) { // oh well } } } }
3) 在第一次启动的时候在PhoneStatusBar中添加Navigation bar的调用逻辑
public void showNavigationBar() { Xlog.d(TAG, " showNavigationBar "); if (mFirstBoot) { if (mNavigationBarView == null) { try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Slog.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { mNavigationBarView = (NavigationBarView) View.inflate(mContext, R.layout.navigation_bar, null); mNavigationBarView.setDisabledFlags(mDisabled); mNavigationBarView.setBar(this); } } catch (RemoteException ex) { // no window manager? good luck with that Xlog.e(TAG, " RemoteException error happened, [can't find WindowManager]"); return; } } addNavigationBar(); mFirstBoot = false; return; } if (mNavigationBarView == null) { try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Slog.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { mNavigationBarView = (NavigationBarView) View.inflate(mContext, R.layout.navigation_bar, null); mNavigationBarView.setDisabledFlags(mDisabled); mNavigationBarView.setBar(this); } mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams()); } catch (RemoteException ex) { // no window manager? good luck with that } } }
实际中,这样的修改还是有问题的,由于mNavigationBarView不断的释放和创建,会发生某些类似:
01-06 18:04:59.024 14261 14261 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@425d2d50 -- another window of this type already exist 的错误.
这个问题的原因其实和PhoneWindowManager中mNavigationBar相关,在Navigation bar被移除后,没有及时的把mNavigationBar置为空,调用 removeWindowLw(mNavigationBar);
这样就不会出现这个错误了,mNavigationBar的重新赋值会在mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());之后
所以Navigation bar 除了在Phonestatus中有添加View外,实际在PhoneWindowManager中也有对应的对象。之所以有这个对象,是因为Statusbar和Navigationbar和其它的Window不一样,在PhoneWindowManager中,会对Statusbar和Navigationbar单独的计算其Frame。
对statusbar和Navbar 的frame的计算是在beginLayoutLw() 中,而对于其它的任何Window frame的计算是layoutWindow() 中,所以在layoutWindow中有如下的语句:
// we've already done the status bar ,return directly cause they will be processed in beginLayoutLw. if (win == mStatusBar || win == mNavigationBar) { return; }
所以这里mNavigationBar的修改,会对layoutWindow() 是否layout Navbar的判断产生影响,例如应该加上这样的判断,保证不再对Navbar进行layout,这样对大大的提高效率和程序的稳定性
if(win.getAttrs().getTitle().toString().contains("NavigationBar")) { return; }
Navbar 的隐藏和显示可以通过setSystemUiVisibility这个接口来改变,通常情况下Navbar要么显示,要么不显示,当Navbar显示的时候,有一些情况是需要隐藏Navbar的,最长见的例子就是视频播放的时候,所以Android提供了这个接口。
在设置和显示隐藏Navbar的过程中,出现了一个问题,就是在播放视频的时候,不再能全屏显示,而是所谓的LOW_PROFILE模式(边框的layout存在,变黑,有3个黑点),这是事先没有预想到的,
下面说一下解决的办法:
1) 右边的边框的layout还存在,所以说明窗口的Frame还存在,而窗口Frame是在PhoneWindowManager中发起并且计算的,所以可能和PMW这块有关。
2) 为什么是LOW_PROFILE模式呢,这个可以在PhoneStatusBar中的同名setSystemUiVisibility的同名函数找到答案。
其实问题的原因就是和PWM中 mHasNavigationBar有关系,这个变量的改变会影响mCanHideNavigationBar的值,而这个值为false的时候,Navbar是不会消失的,问题的原因其实就是mCanHideNavigationBar导致的
遇到问题并不可怕,关键是解决问题的思路对不对。
Recents界面是有Navbar的,而且它是透明的,在它下面是WallPaper,当在Recents界面在重新打开一个应用的时候,实际上会调用moveToFront这个函数,先把HomeActvity显示出来然后再更新应用界面,在Home界面是没有Actionbar的所以就会导致Navbar先消失再重新再显现。
知道和home以及moveToFront相关,修改其实也简单:
final void moveTaskToFrontLocked(TaskRecord tr, ActivityRecord reason, Bundle options) { // frank: Launch recents app (moveToFront) cause Navigation bar flick @{ if (reason != null && reason.isHomeActivity) { mHasMoveHomeToFront = true; Slog.d(TAG, " moveTaskToFrontLocked mHasMoveHomeToFront =" + mHasMoveHomeToFront); } ... }
在需要的使用mHasMoveHomeToFront就可以了。
1.本来想在WMS中加入下面的代码,以想通过systemUiVisibility这个接口来控制Navigation bar的layout,结果发现不行,可能是android对这种模式不支持所致
relayoutWindow@WMS if (attrs != null && seq == win.mSeq) { win.mSystemUiVisibility = systemUiVisibility; /// frank //if (win.mAttrs.packageName.contains("com.your.pacakgename")) { // Slog.d(TAG" , " hide SYSTEM_UI_FLAG_HIDE_NAVIGATION systemUiVisibility = " // + Integer.toHexString(systemUiVisibility) // + " win.mSystemUiVisibility = " + Integer.toHexString(win.mSystemUiVisibility)); // // systemUiVisibility |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; // if ((win.mSystemUiVisibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { // win.mSystemUiVisibility |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; // //win.mLayoutNeeded = true; // } //} }
setSystemUiVisibility hideNavigationBar@StatusbarManagerService //boolean visible = false; //int mask = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // 400 // | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // 200 // | View.SYSTEM_UI_FLAG_LAYOUT_STABLE //100 // | View.SYSTEM_UI_FLAG_LOW_PROFILE //1 // | View.SYSTEM_UI_FLAG_FULLSCREEN // 4 // | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; //2 //int newVis = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; //if (!visible) { // //newVis |= View.SYSTEM_UI_FLAG_LOW_PROFILE // // | View.SYSTEM_UI_FLAG_FULLSCREEN // // | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; // newVis |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; //} //setSystemUiVisibility(newVis, 0xffffffff);
3. 最终采用的还是addView和 removeView来实现的,需要注意的是:addView和removeView都会导致窗口的重新layout,所以用起来还是很方便。
20150514
ANDROID学习笔记系列
--------------------------------------------
联系方式
--------------------------------------------
Weibo: ARESXIONG
E-Mail: [email protected]
------------------------------------------------