1.NavigationBar定义
我们使用的大多数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);
}
}
其中的addNavigationBar() 具体的实现方法如下:
// 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());
}
可以看到Navigationbar 实际上是windowmanager 向window 窗口里添加一个view 。在调用addNavigationBar() 方法之前会 回调start() 的父方法super.start() 来判断是否要添加NavigationBar 。在super.start() 的调用父类方法里会调用createAndAddWindows() ,该方法内会判断是否需要添加显示NavigationBar, 然后决定是否要实例化NavigationBarView.
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;
}
从上面代码可以看到 mHasNavigationBar 的值的设定 是由两处决定的:
1. 首先从系统的资源文件中取设定值config_showNavigationBar, 这个值的设定的文件路径是 frameworks/base/core/res/res/values/config.xml
<!-- Whether a software navigation bar should be shown. NOTE: in the future this may be
autodetected from the Configuration. -->
<bool name="config_showNavigationBar">false</bool>
2. 然后系统要获取“ qemu.hw.mainkeys” 的值,这个值可能会覆盖上面获取到的mHasNavigationBar 的值。如果 “qemu.hw.mainkeys” 获取的值不为空的话,不管值是true 还是false, 都要依据后面的情况来设定。
PhoneWindowManager.java中:
String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
该配置项所在目录一般在:/system/ build.prop中。
所以上面的两处设定共同决定了NavigationBar的显示与隐藏。
2.显示与隐藏
现在要说的显示与隐藏,并不是指在开机的时候,这可以在 xml 中设置,如上,不详述。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;
}
3.Launcher与应用之间切换
1
) 对上面的方式进行总结
从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的变化如下:
ensureActivityConfigurationLocked@ActivityStack.java
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;
}
....
}
}
4.系统重启Launcher界面显示Nv Bar
在实际的应用中,发现没有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 的错误.
5.问题:another window of this type already exists
这个问题的原因其实和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;
}
6.问题:setSystemUiVisibility接口异常
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导致的
遇到问题并不可怕,关键是解决问题的思路对不对。
7.问题: Recents启动应用Frame异常和Navbar闪烁
首先需要说明一下, 修改过程中遇到问题,很多的时候都是和Recents 这个界面有关系,Recets 界面也是在SystemUI 中,具体没有详细的研究。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就可以了。
8.问题: 修改尝试值之原因与总结
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: aresxdy@gmail.com
------------------------------------------------