NavigationBar分析與常見問題


1.NavigationBar定義

咱們使用的大多數android 手機上的Home 鍵,返回鍵以及menu 鍵都是實體觸摸感應按鍵。若是你用Google Nexus4 Nexus5 話,你會發現它們並無實體按鍵或觸摸感應按鍵,取而代之的是在屏幕的下方加了一個小黑條,在這個黑條上有3 個按鈕控件,這種設置無疑使得手機的外觀的設計更加簡約。但我遇到身邊用Nexus 4 手機的人都吐槽這種設計,緣由很簡單:好端端的屏幕,被劃出一塊區域用來顯示3 個按鈕:Back, Home, Recent 。而且它一直用在那裏佔用着。 java

android 源碼中,那一塊區域被叫作NavigationBar 。同時,google 在代碼中也預留了標誌,用來控制它的顯示與隱藏。NavigationBar 的顯示與隱藏的控制是放在SystemUI 中的,具體的路徑是:\frameworks\base\packages\SystemUI 。對android4.0 以上的手機而言, SystemUi 包含兩部分:StatusBar NavigationBar 。在SystemUI 的工程下有一個類PhoneStatusBar.java ,在該類中能夠發現關於控制NavigationBar 的相關代碼: android

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中: app

String navBarOverride = SystemProperties.get("qemu.hw.mainkeys"); ide

該配置項所在目錄通常在:/system/ build.prop中。
  因此上面的兩處設定共同決定了NavigationBar的顯示與隱藏。 函數

2.顯示與隱藏

如今要說的顯示與隱藏,並非指在開機的時候,這能夠在 xml 中設置,如上,不詳述。

NavigationBar能夠在開機後根據須要顯示或隱藏,好比在打開某個應用隱藏,打開另外一應用顯示。 學習

修改步驟: ui

1ActivityStack.java中的 resumeTopActivityLocked 是全部啓動應用的啓動的入口,因此在這裏添加進入的入口。 this

2) 在PhoneWindowManager.java中的 mHasNavigationBar 是顯示與否的標誌,確定要修改,而這裏的修改應該在WindowManagerService.java中進行,由於,WindowManagerService中的mPolicy是操做PhoneWindowManager的接口,這樣不會破壞封裝,因此1)中要添加調入到WMS中的接口,WMS而後再調入PWM google

3PWMPhoneWindowManager)中有mStatusBarService,之因此用這個服務,是由於不破壞封裝和同步。 spa

4StatusBarManagerService 中添加顯示消失的接口,同理在Client端也要添加相應的顯示和消失接口,具體在 CommandQueuePhoneStatusBar

5PhoneStatusBar中添加顯示和消失的邏輯。

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 barandroid的原生應用有NV bar,發現一個問題,在從MMs返回自寫Launcher時,會從新調用LauncheronCreate函數,致使本來進入了GridView的,結果停留在Home的壁紙界面;而其它的應用如計算器就會直接進如gridview頁面。問題的緣由就是因爲應用的fullscreenLauncher Configuration變化引發的。

a. 解釋下MMs和計算器的區別,實際上就是fullscreen的區別,一般來講和透明度相關

詳細參見ActivityRecord()@ActivityRecord.java

MMs: fullscreen == false

Caculator: fullscreen == true

b. 解釋下上面問題的緣由,想象一下這樣的情景,從GridView進入MMS中,

1LauncherfullscreenPaused

2launch MMsResumed

3)因爲MMs不是fullscreen,因此在ensureActivitiesVisibleLocked中會去檢查當前的top應用是否全屏,若是不是全屏,則會把下邊的Acitivity show出來,此刻,下面的ActivityLauncher,並且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

也不會刷新屏幕,從而進入launcheronCeate流程,緣由在下面就返回了:

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 可能爲nullStatusBarManagerService 依賴WMS(參見SystemServer.java), WMS中才能調用PWM,而hideNavigation Bar又是從PWM中調入,必然在中間PWMmStatusBarSerivice有時刻爲空,致使會先顯示navigation bar而後再消失。

解決方案:

1) 取消默認的navigation bar顯示 【在config.xml 中取消】

2PWM中判斷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 

這個問題的緣由其實和PhoneWindowManagermNavigationBar相關,Navigation bar被移除後,沒有及時的把mNavigationBar置爲空,調用 removeWindowLw(mNavigationBar);

這樣就不會出現這個錯誤了,mNavigationBar的從新賦值會在mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());以後

因此Navigation bar 除了在Phonestatus中有添加View外,實際在PhoneWindowManager中也有對應的對象。之因此有這個對象,是由於StatusbarNavigationbar和其它的Window不同,在PhoneWindowManager中,會對StatusbarNavigationbar單獨的計算其Frame

statusbarNavbar 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的同名函數找到答案。

其實問題的緣由就是和PWMmHasNavigationBar有關係,這個變量的改變會影響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 barlayout,結果發現不行,多是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. 最終採用的仍是addViewremoveView來實現的,須要注意的是:addViewremoveView都會致使窗口的從新layout,因此用起來仍是很方便。




20150514


ANDROID學習筆記系列

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

                    聯繫方式

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

        Weibo: ARESXIONG

        E-Mail: aresxdy@gmail.com

------------------------------------------------
相關文章
相關標籤/搜索