NavigationBar分析与常见问题


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
  }


WindowManagerService 类实现了WindowManagerPolicy 的接口,所以 WindowManagerService 会回调WindowManagerPolicy hasNavigationBar() 接口

  
@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;
  }


而mHasNavigationBar 的赋值可以在PhoneWindowManager 中的setInitialDisplaySize(Display display, int width, int height, int density) 方法中找到,
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;
    //    }
    //}
}


2. 想在StatusManagerService 中设置Navigation bar 的显示和隐藏,由于在修改setSystemUiVisibility 接口异常问题没解决的时候尝试的,所以没有效果,理论上是能行的,关于Navbar 的消失和显示,可以在android 的Demo App 中找到例子
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

------------------------------------------------


转载于:https://my.oschina.net/u/2288529/blog/414755

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值